diff --git a/.devcontainer/Containerfile.dev b/.devcontainer/Containerfile.dev new file mode 100644 index 00000000..f188e6f4 --- /dev/null +++ b/.devcontainer/Containerfile.dev @@ -0,0 +1,24 @@ +FROM python:3.10-slim-buster + +RUN apt-get update && apt-get install -y \ + build-essential \ + gcc \ + && apt-get clean + +WORKDIR /workspace + +COPY requirements.txt requirements-dev.txt requirements-scancode.txt /tmp/ + +RUN pip3 install --no-cache-dir -r /tmp/requirements.txt && \ + pip3 install --no-cache-dir -r /tmp/requirements-dev.txt && \ + pip3 install --no-cache-dir scanoss_winnowing && \ + pip3 install --no-cache-dir scancode-toolkit-mini + +# Download compile and install typecode-libmagic from source (as there is not ARM wheel available) +ADD https://github.com/nexB/typecode_libmagic_from_sources/archive/refs/tags/v5.39.210212.tar.gz /install/ +RUN tar -xvzf /install/v5.39.210212.tar.gz -C /install \ + && cd /install/typecode_libmagic_from_sources* \ + && ./build.sh && python3 setup.py sdist bdist_wheel \ + && pip3 install --user `ls /install/typecode_libmagic_from_sources*/dist/*.whl` + +CMD ["sleep", "infinity"] diff --git a/.devcontainer/devcontainer.example.json b/.devcontainer/devcontainer.example.json new file mode 100644 index 00000000..496f43d6 --- /dev/null +++ b/.devcontainer/devcontainer.example.json @@ -0,0 +1,42 @@ +{ + "name": "SCANOSS Dev Container", + "build": { + "dockerfile": "Containerfile.dev", + "context": ".." + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.black-formatter", + "ms-python.isort", + "ms-python.flake8" + ], + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.formatting.provider": "black", + "python.formatting.blackPath": "/usr/local/bin/black", + "python.linting.enabled": true, + "python.linting.flake8Enabled": true, + "python.linting.pylintEnabled": false, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + }, + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false + } + } + }, + "mounts": [ + "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached" + ], + "postCreateCommand": "make dev_setup", + "features": { + "ghcr.io/devcontainers/features/python:1": { + "version": "3.11" + }, + "ghcr.io/devcontainers/features/git:1": {} + } +} \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..1c601a3a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: "/" + schedule: + interval: monthly diff --git a/.github/workflows/container-local-test.yml b/.github/workflows/container-local-test.yml new file mode 100644 index 00000000..053f794b --- /dev/null +++ b/.github/workflows/container-local-test.yml @@ -0,0 +1,107 @@ +name: Build/Test Local Container +# Build a docker image on demand and run a local test (connecting to api.osskb.org) + +on: + workflow_dispatch: + push: + branches: + - "main" + pull_request: + branches: + - "main" + +env: + IMAGE_BASE: scanoss/scanoss-py-base + IMAGE_NAME: scanoss/scanoss-py + IMAGE_JENKINS: scanoss/scanoss-py-jenkins + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + # Setup and build the python package + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.9.x" + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Build Package + run: make dist + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v3 + + # Build Docker image with Buildx - Base + - name: Build Docker Image - No Entrypoint + id: build-no-ep + uses: docker/build-push-action@v5 + with: + context: . + push: false + tags: ${{ env.IMAGE_BASE }}:latest + target: no_entry_point + outputs: type=docker,dest=/tmp/scanoss-py-base.tar + + # Build Docker image with Buildx - Jenkins + - name: Build Docker Image - Jenkins + id: build-je + uses: docker/build-push-action@v5 + with: + context: . + push: false + tags: ${{ env.IMAGE_JENKINS }}:latest + target: jenkins + outputs: type=docker,dest=/tmp/scanoss-py-jenkins.tar + + # Build Docker image with Buildx - Entrypoint + - name: Build Docker Image - With Entrypoint + id: build-with-ep + uses: docker/build-push-action@v5 + with: + context: . + push: false + tags: ${{ env.IMAGE_NAME }}:latest + target: with_entry_point + outputs: type=docker,dest=/tmp/scanoss-py.tar + + - name: Test Docker Image - No Entrypoint + run: | + docker load --input /tmp/scanoss-py-base.tar + docker image ls -a + docker run ${{ env.IMAGE_BASE }} scanoss-py version + + - name: Test Docker Image - Jenkins + run: | + docker load --input /tmp/scanoss-py-jenkins.tar + docker image ls -a + docker run ${{ env.IMAGE_JENKINS }} scanoss-py version + + - name: Test Docker Image - With Entrypoint + run: | + docker load --input /tmp/scanoss-py.tar + docker image ls -a + docker run ${{ env.IMAGE_NAME }} version + docker run ${{ env.IMAGE_NAME }} utils fast + docker run -e SCANOSS_API_KEY="${{ secrets.SC_API_KEY }}" -v "$(pwd)":"/scanoss" ${{ env.IMAGE_NAME }} scan -o results.json tests + id_count=$(cat results.json | grep '"id":' | wc -l) + echo "ID Count: $id_count" + if [[ $id_count -lt 1 ]]; then + echo "Error: Scan test did not produce any results. Failing" + exit 1 + fi + docker run -v "$(pwd)":"/scanoss" ${{ env.IMAGE_NAME }} wfp --skip-headers -o fingers.wfp tests + wfp_count=$(cat fingers.wfp | grep 'file=' | wc -l) + echo "WFP Count: $wfp_count" + if [[ $wfp_count -lt 1 ]]; then + echo "Error: WFP test did not produce any results. Failing" + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/container-publish-ghcr.yml b/.github/workflows/container-publish-ghcr.yml new file mode 100644 index 00000000..84aebab0 --- /dev/null +++ b/.github/workflows/container-publish-ghcr.yml @@ -0,0 +1,169 @@ +name: Publish GHCR Container +# Publish a multi-platform container when a version is tagged + +on: + workflow_dispatch: + push: + tags: + - "v*.*.*" + +env: + REGISTRY: ghcr.io + IMAGE_NAME_BASE: scanoss/scanoss-py-base + IMAGE_NAME: scanoss/scanoss-py + IMAGE_JENKINS: scanoss/scanoss-py-jenkins + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # This is used to complete the identity challenge with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + # Setup and build python package + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9.x' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Build package + run: make dist + + # Add support for more platforms with QEMU + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # Workaround: https://github.com/docker/build-push-action/issues/461 + # uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v3 + + # Login against a Docker registry except on PR + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + - name: Extract Docker metadata - no entrypoint + id: meta-ne + uses: docker/metadata-action@v4 + with: + images: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BASE }}" + + # Build and push Docker image with Buildx (don't push on PR) + - name: Build and push Docker image - Base (no entrypoint) + id: build-and-push-ne + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta-ne.outputs.tags }} + labels: ${{ steps.meta-ne.outputs.labels }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: false + target: no_entry_point + + # Extract metadata (tags, labels) for Docker + - name: Extract Docker metadata - jenkins + id: meta-je + uses: docker/metadata-action@v4 + with: + images: "${{ env.REGISTRY }}/${{ env.IMAGE_JENKINS }}" + + # Build and push Docker image with Buildx (don't push on PR) + - name: Build and push Docker image - Jenkins + id: build-and-push-je + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta-je.outputs.tags }} + labels: ${{ steps.meta-je.outputs.labels }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: false + target: jenkins + + # Extract metadata (tags, labels) for Docker + - name: Extract Docker metadata - entrypoint + id: meta + uses: docker/metadata-action@v4 + with: + images: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" + + # Build and push Docker image with Buildx (don't push on PR) + - name: Build and push Docker image - EP (entrypoint) + id: build-and-push + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: false + target: with_entry_point + + # Test the docker image + - name: Test Published Image + if: github.event_name != 'pull_request' + run: | + docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + docker run ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} version + docker run ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} utils fast + docker run -e SCANOSS_API_KEY="${{ secrets.SC_API_KEY }}" -v "$(pwd)":"/scanoss" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} scan -o results.json tests + id_count=$(cat results.json | grep '"id":' | wc -l) + echo "ID Count: $id_count" + if [[ $id_count -lt 1 ]]; then + echo "Error: Scan test did not produce any results. Failing" + exit 1 + fi + docker run -v "$(pwd)":"/scanoss" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} wfp --skip-headers -o fingers.wfp tests + wfp_count=$(cat fingers.wfp | grep 'file=' | wc -l) + echo "WFP Count: $wfp_count" + if [[ $wfp_count -lt 1 ]]; then + echo "Error: WFP test did not produce any results. Failing" + exit 1 + fi + +# Install the cosign tool except on PR +# - name: Install cosign +# if: github.event_name != 'pull_request' +# uses: sigstore/cosign-installer@v2 +# +# - name: Check Cosign Version +# run: cosign version +# +# - name: Sign Docker Image +# if: ${{ github.event_name != 'pull_request' }} +# env: +# TAGS: ${{ steps.meta.outputs.tags }} +# COSIGN_PRIVATE_KEY: ${{secrets.COSIGN_PRIVATE_KEY}} +# COSIGN_PASSWORD: ${{secrets.COSIGN_PASSWORD}} +# run: cosign sign --key env://COSIGN_PRIVATE_KEY --no-tlog-upload=true ${TAGS} + +# - name: Sign the images with GitHub OIDC Token +# run: cosign sign ${TAGS} +# env: +# TAGS: ${{ steps.meta.outputs.tags }} +# COSIGN_EXPERIMENTAL: true + diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..11367ddc --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,29 @@ +name: Lint + +on: + pull_request: + branches: [main] + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.9" + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + + - name: Run Ruff on changed files + run: | + make lint diff --git a/.github/workflows/python-local-test.yml b/.github/workflows/python-local-test.yml new file mode 100644 index 00000000..59aa6813 --- /dev/null +++ b/.github/workflows/python-local-test.yml @@ -0,0 +1,109 @@ +name: Build/Test Local Python Package +# This workflow will upload a TestPyPI Python Package using Twine on demand (dispatch) + +on: + workflow_dispatch: + push: + branches: + - "main" + pull_request: + branches: + - "main" + +permissions: + contents: read + +env: + SCANOSS_API_KEY: ${{ secrets.SC_API_KEY }} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.9.x" + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Build Local Package + run: make dist + + - name: Install Test Package + uses: nick-fields/retry@v3 + with: + timeout_minutes: 2 + retry_wait_seconds: 10 + max_attempts: 3 + retry_on: error + shell: bash + command: | + pip install -r requirements.txt + pip install dist/scanoss-*.whl + which scanoss-py + + - name: Run Tests + run: | + which scanoss-py + scanoss-py version + scanoss-py utils fast + scanoss-py scan tests > results.json + id_count=$(cat results.json | grep '"id":' | wc -l) + echo "ID Count: $id_count" + if [[ $id_count -lt 1 ]]; then + echo "Error: Scan test did not produce any results. Failing" + exit 1 + fi + scanoss-py wfp --skip-headers tests > fingers.wfp + wfp_count=$(cat fingers.wfp | grep 'file=' | wc -l) + echo "WFP Count: $wfp_count" + if [[ $wfp_count -lt 1 ]]; then + echo "Error: WFP test did not produce any results. Failing" + exit 1 + fi + + - name: Run Tests (fast winnowing) + run: | + pip install scanoss_winnowing + which scanoss-py + scanoss-py version + scanoss-py utils fast + scanoss-py scan tests > results.json + id_count=$(cat results.json | grep '"id":' | wc -l) + echo "ID Count: $id_count" + if [[ $id_count -lt 1 ]]; then + echo "Error: Scan test did not produce any results. Failing" + exit 1 + fi + scanoss-py wfp --skip-headers tests > fingers.wfp + wfp_count=$(cat fingers.wfp | grep 'file=' | wc -l) + echo "WFP Count: $wfp_count" + if [[ $wfp_count -lt 1 ]]; then + echo "Error: WFP test did not produce any results. Failing" + exit 1 + fi + + - name: Run Tests HPSM (fast winnowing) + run: | + pip install scanoss_winnowing + which scanoss-py + scanoss-py version + scanoss-py utils fast + scanoss-py wfp -H tests > fingers.wfp + wfp_count=$(cat fingers.wfp | grep 'file=' | wc -l) + echo "WFP Count: $wfp_count" + if [[ $wfp_count -lt 1 ]]; then + echo "Error: WFP test did not produce any results. Failing" + exit 1 + fi + + - name: Run Unit Tests + run: | + python -m unittest + diff --git a/.github/workflows/python-publish-pypi.yml b/.github/workflows/python-publish-pypi.yml new file mode 100644 index 00000000..1dc8b571 --- /dev/null +++ b/.github/workflows/python-publish-pypi.yml @@ -0,0 +1,144 @@ +name: Publish Python Package - PyPI +# This workflow will upload a Python Package using Twine to PyPI and create a draft release when a tag is pushed + +on: + workflow_dispatch: + push: + tags: + - "v*.*.*" + +env: + SCANOSS_API_KEY: ${{ secrets.SC_API_KEY }} + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Build Package - ${{ github.ref_name }} + run: make dist + + - name: Install Test Package + run: | + pip install -r requirements.txt + pip install dist/scanoss-*-py3-none-any.whl + which scanoss-py + + - name: Run Unit Tests + run: | + make unit_test + + - name: Run Local Tests + run: | + which scanoss-py + scanoss-py version + scanoss-py utils fast + scanoss-py scan tests > results.json + id_count=$(cat results.json | grep '"id":' | wc -l) + echo "ID Count: $id_count" + if [[ $id_count -lt 1 ]]; then + echo "Error: Scan test did not produce any results. Failing" + exit 1 + fi + scanoss-py wfp --skip-headers tests > fingers.wfp + wfp_count=$(cat fingers.wfp | grep 'file=' | wc -l) + echo "WFP Count: $wfp_count" + if [[ $wfp_count -lt 1 ]]; then + echo "Error: WFP test did not produce any results. Failing" + exit 1 + fi + pip uninstall -y scanoss + + - name: Publish Package - ${{ github.ref_name }} + uses: pypa/gh-action-pypi-publish@release/v1 + with: + # skip-existing: true + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + + - name: Create Draft Release ${{ github.ref_type }} - ${{ github.ref_name }} + if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') + uses: softprops/action-gh-release@v1 + with: + draft: true + files: dist/* + + test: + if: success() + needs: [deploy] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9.x' + + - name: Install Remote Package + uses: nick-fields/retry@v3 + with: + timeout_minutes: 3 + retry_wait_seconds: 10 + max_attempts: 3 + retry_on: error + command: | + scanoss_version=$(python ./version.py) + echo "Sleeping before checking PyPI for new release version ${scanoss_version}..." + sleep 60 + echo "Installing scanoss ${scanoss_version}..." + pip install --upgrade scanoss==$scanoss_version + which scanoss-py + + - name: Run Tests + run: | + which scanoss-py + scanoss-py version + scanoss-py utils fast + scanoss-py scan tests > results.json + id_count=$(cat results.json | grep '"id":' | wc -l) + echo "ID Count: $id_count" + if [[ $id_count -lt 1 ]]; then + echo "Error: Scan test did not produce any results. Failing" + exit 1 + fi + scanoss-py wfp --skip-headers tests > fingers.wfp + wfp_count=$(cat fingers.wfp | grep 'file=' | wc -l) + echo "WFP Count: $wfp_count" + if [[ $wfp_count -lt 1 ]]; then + echo "Error: WFP test did not produce any results. Failing" + exit 1 + fi + + - name: Run Tests (fast winnowing) + run: | + pip install scanoss_winnowing + which scanoss-py + scanoss-py version + scanoss-py utils fast + scanoss-py scan tests > results.json + id_count=$(cat results.json | grep '"id":' | wc -l) + echo "ID Count: $id_count" + if [[ $id_count -lt 1 ]]; then + echo "Error: Scan test did not produce any results. Failing" + exit 1 + fi + scanoss-py wfp --skip-headers tests > fingers.wfp + wfp_count=$(cat fingers.wfp | grep 'file=' | wc -l) + echo "WFP Count: $wfp_count" + if [[ $wfp_count -lt 1 ]]; then + echo "Error: WFP test did not produce any results. Failing" + exit 1 + fi + diff --git a/.github/workflows/python-publish-testpypi.yml b/.github/workflows/python-publish-testpypi.yml new file mode 100644 index 00000000..d44c02a0 --- /dev/null +++ b/.github/workflows/python-publish-testpypi.yml @@ -0,0 +1,107 @@ +name: Publish Python Package - TestPyPI +# This workflow will upload a TestPyPI Python Package using Twine on demand (dispatch) + +on: [workflow_dispatch] + +permissions: + contents: read + +env: + SCANOSS_API_KEY: ${{ secrets.SC_API_KEY }} + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9.x' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Build Package + run: make dist + + - name: Install Test Package + run: | + pip install -r requirements.txt + scanoss_version=$(python ./version.py) + echo "Local test install of scanoss ${scanoss_version}..." + pip install dist/scanoss-*-py3-none-any.whl + which scanoss-py + + - name: Run Local Tests + run: | + which scanoss-py + scanoss-py version + scanoss-py scan tests > results.json + id_count=$(cat results.json | grep '"id":' | wc -l) + echo "ID Count: $id_count" + if [[ $id_count -lt 1 ]]; then + echo "Error: Scan test did not produce any results. Failing" + exit 1 + fi + pip uninstall -y scanoss + + - name: Publish Test Package + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true + user: __token__ + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + repository-url: https://test.pypi.org/legacy/ + + test: + if: success() + needs: [deploy] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9.x' + + - name: Install Remote Package + run: | + scanoss_version=$(python ./version.py) + pip install -r requirements.txt + echo "Install TestPyPI scanoss ${scanoss_version}..." + pip install -i https://test.pypi.org/simple/ --upgrade scanoss==${scanoss_version} + which scanoss-py + + - name: Run Tests + run: | + which scanoss-py + scanoss-py version + scanoss-py utils fast + scanoss-py scan tests > results.json + id_count=$(cat results.json | grep '"id":' | wc -l) + echo "ID Count: $id_count" + if [[ $id_count -lt 1 ]]; then + echo "Error: Scan test did not produce any results. Failing" + exit 1 + fi + + - name: Run Tests (fast winnowing) + run: | + pip install scanoss_winnowing + which scanoss-py + scanoss-py version + scanoss-py utils fast + scanoss-py scan tests > results.json + id_count=$(cat results.json | grep '"id":' | wc -l) + echo "ID Count: $id_count" + if [[ $id_count -lt 1 ]]; then + echo "Error: Scan test did not produce any results. Failing" + exit 1 + fi + diff --git a/.github/workflows/scanoss.yml b/.github/workflows/scanoss.yml new file mode 100644 index 00000000..d9dcb338 --- /dev/null +++ b/.github/workflows/scanoss.yml @@ -0,0 +1,31 @@ +name: SCANOSS + +on: + pull_request: + push: + branches: + - "*" + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + checks: write + actions: read + +jobs: + scanoss-code-scan: + name: SCANOSS Code Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run SCANOSS Code Scan + id: scanoss-code-scan-step + uses: scanoss/code-scan-action@v1 + with: + policies: undeclared + api.url: https://api.scanoss.com/scan/direct + api.key: ${{ secrets.SC_API_KEY }} diff --git a/.github/workflows/version-tag.yml b/.github/workflows/version-tag.yml new file mode 100644 index 00000000..55afecb0 --- /dev/null +++ b/.github/workflows/version-tag.yml @@ -0,0 +1,42 @@ +name: Repo Version Tagging +# This workflow will read the version details from the repo and apply a branch + +on: + workflow_dispatch: + inputs: + run_for_real: + required: true + default: false + type: boolean + description: "Apply next tag (or Dry Run)" + +concurrency: production + +jobs: + version-tagging: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: "0" + token: ${{ secrets.SC_GH_TAG_TOKEN }} + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9.x' + - name: Determine Tag + id: taggerVersion + run: | + app_version=$(tools/get_next_version.sh) + echo "New Proposed tag: $app_version" + echo "package_app_version=$app_version" >> $GITHUB_ENV + + - name: Apply Tag + if: ${{ inputs.run_for_real }} + id: taggerApply + run: | + echo "Applying tag ${{env.package_app_version}} ..." + git tag "${{env.package_app_version}}" + echo "Pushing changes..." + git push --tags + diff --git a/.gitignore b/.gitignore index 9607215a..8fc669fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,37 @@ *.wfp *-result.json +*-res.json +*.pem .vscode/ gitee_com_* github.com_* master.zip dist/ build/ +tmp/ .eggs *.egg-info __pycache__ venv/ .idea +src/scanoss/data/build_date.txt +bad*.txt +*.csv +*.json +*.tar +*.tgz +*.gz +*.zip +local-*.txt +docs/build +.devcontainer/devcontainer.json +!.devcontainer/*.example.json + +!tests/data/*.json +!tests/data/header-files-test.zip +!docs/source/_static/*.json +!scanoss-settings-schema.json +.DS_Store +!scanoss.json +examples/output/ +!spdx-*.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..da718915 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.4 + hooks: + - id: ruff + - id: ruff-format + - repo: https://github.com/scanoss/pre-commit-hooks + rev: v0.2.0 + hooks: + - id: scanoss-check-undeclared-code + diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..6d98edfe --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,32 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: '3.12' + # You can also specify other tool versions: + # nodejs: "19" + # rust: "1.64" + # golang: "1.19" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/source/conf.py + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements-docs.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index bd796f02..27c34a75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,686 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + + +## [1.52.1] - 2026-04-14 +### Fixed +- Fixed CPE identifiers missing from SPDX Lite output (`--format spdxlite`) + - CPEs are now emitted as SPDX 2.2 `externalRefs` with `referenceCategory: SECURITY` + - CPE 2.3 strings use `referenceType: cpe23Type`; legacy `cpe:/...` and `cpe:2.2:...` use `cpe22Type` + - Multiple CPEs per component are preserved and deduplicated + +## [1.52.0] - 2026-04-09 +### Added +- Added `status` subcommand query to `component` command to retrieve development life-cycle status: + - Component and version-specific for a single component + - Component and version-specific for a list of components + +## [1.51.1] - 2026-04-01 +### Fixed +- Fixed vulnerabilities not appearing in CycloneDX output for folder-scan (`fs`) command +- Fixed CycloneDX output without vulnerabilities being printed to stdout when using `--output` with folder-scan + +## [1.51.0] - 2026-03-26 +### Added +- Added `--format raw` option to `folder-scan` command to export HFH results in snippet-scanner JSON format + - Expands directory-level HFH results into per-file entries keyed by relative file path + - Assigns each file to the most specific matching `path_id` (deepest directory match wins) +- Added license decoration to folder hash scan results via dependency service + - Each component version in HFH results is now decorated with license information + - CycloneDX output uses pre-decorated licenses instead of making a separate dependency API call + +## [1.50.1] - 2026-03-23 +### Fixed +- Fixed `bom.replace` rules with a `license` field: the license is now applied to the replaced result instead of being silently dropped + +## [1.50.0] - 2026-03-17 +### Fixed +- Fixed `requirement` field being lost during dependency decoration in scan command +- Sanitized scancode `extracted_requirement` to strip redundant package name prefix (e.g., `gtest==1.17.0` → `1.17.0`) + +## [1.49.1] - 2026-03-17 +### Fixed +- When an error occurs during the scan, do not write a partial scan result file. Leave it empty. + +## [1.49.0] - 2026-03-12 +### Fixed +- Fixed `--skip-headers` incorrectly identifying continuation lines inside multi-line import blocks +### Changed +- Added error handling for `is_binary` check to catch `RuntimeError` and default to treating the file as binary + +## [1.48.0] - 2026-03-06 +### Added +- Added `--apiurl` option to all component subcommands (`comp vulns`, `comp licenses`, `comp semgrep`, `comp provenance`, `comp search`, `comp versions`) to allow overriding the default API base URL + +## [1.47.0] - 2026-03-05 +### Added +- Added support for skipping dependency files, configurable via `settings.skip.patterns.dependencies` + +### Changed +- All API communication now uses REST by default +- The `--grpc`, `--rest`, `--api2url`, and `--grpc-proxy` CLI flags now cause an error if used (gRPC is no longer supported) + +## [1.46.0] - 2026-03-04 +### Added +- Added folder-level (path-scoped) BOM filtering for `include`, `exclude`, `remove`, and `replace` rules + - BOM rules can now target specific folders (e.g., `"path": "src/vendor/"`) in addition to individual files + - Priority-based matching: path+purl (highest) > purl-only > path-only (lowest); longer paths win ties + - Path-scoped `include` entries are sent as identify context only for files under the matching folder + - Path-scoped `exclude` entries are sent as blacklist context only for files under the matching folder +- Replace rules now contribute their `replace_with` PURL to the scan context, improving server-side matching +- BOM path matching is agnostic to trailing slashes (`src/vendor/` and `src/vendor` are equivalent) + +### Changed +- Refactored SBOM handling: replaced global SBOM context with per-file `SbomContext` resolution via `ScanossSettings.get_sbom_context()` +- Refactored `BomEntry` from TypedDict to dataclass hierarchy with `matches_path()`, `matches_purl()`, and `priority` support +- Extracted `_iter_wfp_files` generator to simplify WFP parsing in `scan_wfp_file_threaded` + +## [1.45.1] - 2026-02-23 +### Fixed +- Fixed `--input` argument validation for inspect subcommands (copyleft, undeclared, license-summary, component-summary) by making it required at the argparse level instead of manual runtime checks + +## [1.45.0] - 2026-02-02 +### Added +- Added scan engine tuning parameters for snippet matching: + - `--min-snippet-hits` - Minimum snippet hits required (0 defers to server config) + - `--min-snippet-lines` - Minimum snippet lines required (0 defers to server config) + - `--ranking` - Enable/disable result ranking (unset/true/false) + - `--ranking-threshold` - Ranking threshold value (-1 to 10, -1 defers to server config) + - `--honour-file-exts` - Honour file extensions during matching (unset/true/false) +- Added `file_snippet` section to scanoss.json settings schema for configuring tuning parameters +- Added `ScanSettingsBuilder` class for merging CLI and settings file configurations with priority: CLI > file_snippet > root settings + +## [1.44.0] - 2026-01-22 +### Changed +- Refactored `--apiurl` parameter to accept base URLs instead of full endpoint URLs + - Now accepts `https://api.scanoss.com` instead of `https://api.scanoss.com/scan/direct` + - Automatically appends `/scan/direct` endpoint path + - Backward compatible: detects and warns when full endpoint URLs are provided + - Uses `urllib.parse` for robust URL handling (ports, IPv6, encoded characters) + - Updated CLI help text and documentation to reflect base URL format + - Applies to both CLI arguments and `SCANOSS_SCAN_URL` environment variable + +## [1.43.1] - 2026-01-05 +### Changed +- Restored `--no-wfp-output` flag for backwards compatibility (deprecated, no effect) + +## [1.43.0] - 2026-01-02 +### Changed +- Scan command no longer generates `scanner_output.wfp` file +- Removed `--no-wfp-output` flag (no longer needed) + +## [1.42.0] - 2025-12-17 +### Added +- Added support for filtering uninteresting data from the beginning of source files. + - When using `--skip-headers` it will skip over copyright notices, import statements, comments, etc. + - The `--skip-headers-limit` option specifies the maximum number of lines to skip if required. + +## [1.41.1] - 2025-12-16 +- Use `components` instead of `purls` for vulnerability detection when converting to CycloneDX format in Folder Scan + +## [1.41.0] - 2025-11-17 +### Added +- Added `--license-sources` (`-ls`) option to copyleft inspection + - Filter which license sources to check (component_declared, license_file, file_header, file_spdx_tag, scancode) + - Supports both `-ls source1 source2` and `-ls source1 -ls source2` syntax + +### Changed +- **Switched to OSADL authoritative copyleft license data** + - Copyleft detection now uses [OSADL (Open Source Automation Development Lab)](https://www.osadl.org/) checklist data + - Adds missing `-or-later` license variants (GPL-2.0-or-later, GPL-3.0-or-later, LGPL-2.1-or-later, etc.) + - Expands copyleft coverage from 21 to 32 licenses + - Custom include/exclude/explicit filters still use legacy behavior for backward compatibility + - Dataset attribution added to README (CC-BY-4.0 license) + +- Copyleft inspection now defaults to component-level licenses only (component_declared, license_file) + - Reduces noise from file-level license detections (file_header, scancode) + - Use `-ls` to override and check specific sources + +### Fixed +- Fixed the terminal cursor disappearing after aborting scan with Ctrl+C + +## [1.40.1] - 2025-10-29 +### Changed +- Refactored inspect module structure for better organization + - Reorganized inspection modules into `policy_check` and `summary` subdirectories + - Moved copyleft and undeclared component checks to `policy_check/scanoss/` + - Moved component, license, and match summaries to `summary/` + - Moved Dependency Track policy checks to `policy_check/dependency_track/` + - Extracted common scan result processing logic into `ScanResultProcessor` utility class + - Improved type safety with `PolicyOutput` named tuple for policy check results + - Made `PolicyCheck` class explicitly abstract with ABC +### Added +- Added Makefile targets for running ruff linter (`linter`, `linter-fix`, `linter-docker`, `linter-docker-fix`) + +## [1.40.0] - 2025-10-29 +### Added +- Add support for `--rest` to `folder-scan` command + +## [1.39.0] - 2025-10-27 +### Added +- Added `glc-codequality` format to convert subcommand +- Added `inspect gitlab matches` subcommand to generate GitLab-compatible Markdown match summary from SCANOSS scan results +- Added utility modules for shared functionality (`markdown_utils.py` and `file_utils.py`) +### Changed +- Refactored table generation utilities into shared `markdown_utils` module +- Refactored JSON file loading into shared `file_utils` module + +## [1.38.0] - 2025-10-24 +### Added +- Add support for settings debug mode via `SCANOSS_DEBUG` environment variable + +## [1.37.1] - 2025-10-21 +### Added +- Added source filtering to cyclonedx conversion +### Fixed +- Fixed dependencies being skipped during spdx conversion + +## [1.37.0] - 2025-10-17 +### Added +- Added delta folder and file copy command + +## [1.36.0] - 2025-10-08 +### Added +- Add `--recursive-threshold` argument to folder scan command +- Add `--depth` argument to `folder-scan` and `folder-hash` commands + +## [1.35.0] - 2025-10-07 +### Modified +- Use gRPC instead of REST for API calls + +## [1.34.0] - 2025-10-06 +### Added +- Add REST API support for decoration commands + +## [1.33.0] - 2025-09-19 +### Added +- Add `licenses` sub-command to `component` command +- Add support for ingesting CDX to all decoration commands +- Add CDX input validation + +## [1.32.0] - 2025-09-01 +### Added +- Switched vulnerability and dependency APIs to use REST by default + +## [1.31.5] - 2025-08-27 +### Added +- Added jira markdown option for DT +- Added Dependency Track project link to markdown summary +- Updated protobuf client definitions +- Added date field to `scanoss-py comp versions` response + +## [1.31.4] - 2025-08-20 +### Added +- Added support for empty dependency track project policy checks + +## [1.31.3] - 2025-08-19 +### Fixed +- Added handling for empty results files + +## [1.31.2] - 2025-08-12 +### Fixed +- Removed an unnecessary print statement from the policy checker + +## [1.31.1] - 2025-08-08 +### Fixed +- Fixed purl formatting bug in dependency track output + +## [1.31.0] - 2025-08-08 +### Added +- Add `inspect dependency-track project-violations` subcommand to retrieve Dependency Track project violations in Markdown and JSON formats +### Changed +- Renamed `inspect copyleft` to `inspect raw copyleft` +- Renamed `inspect undeclared` to `inspect raw undeclared` +- Renamed `inspect component-summary` to `inspect raw component-summary` +- Renamed `inspect license-summary` to `inspect raw license-summary` +- Updated Policy return codes. 0 → Success, 2 → Fail, 1 → Error +### Fixed +- Fixed incorrect folder filtering configurations for fingerprinting and scanning + +## [1.30.0] - 2025-07-22 +### Added +- Add `export dt` subcommand to export SBOM files to Dependency Track +- Add CycloneDX file validation + +## [1.29.0] - 2025-07-15 +### Changed +- Updated minimum Python version to 3.9 + +## [1.28.3] - 2025-07-14 +### Fixed +- Fixed scanoss.json ingestion +### Added +- Added support for exclude parameter from scanoss.json file during scanning + +## [1.28.2] - 2025-07-14 +### Fixed +- Fix CycloneDX format when license id is None + +## [1.28.1] - 2025-07-10 +### Added +- Fix purls parsing on `crypto` subcommand + +## [1.28.0] - 2025-07-10 +### Added +- Add vulnerabilities response to `folder-scan` CycloneDX output + +## [1.27.1] - 2025-07-09 +### Fixed +- Fixed when running `folder-scan` with `--format cyclonedx` the output was not writing to file +- Fixed when running `container-scan` with `--format cyclonedx` the output was not writing to file + +## [1.27.0] - 2025-06-30 +### Added +- Add directory hash calculation to folder hasher +- Add rank-threshold option to folder scan command + +## [1.26.3] - 2025-06-26 +### Fixed +- Fixed crash in inspect subcommand when processing components that lack license information +- Set the default trace value to false for inspect command + +## [1.26.2] - 2025-06-24 +### Fixed +- Fixed inspection of undeclared components with empty licenses + +## [1.26.1] - 2025-06-23 +### Added +- Added component count to inspect license summary +### Changed +- Modified summaries for inspect subcommand + +## [1.26.0] - 2025-06-20 +### Added +- New `inspect license-summary` subcommand to generate license summaries from scan results +- New `inspect component-summary` subcommand to generate component summaries from scan results +- Added examples for inspect summary commands in CLIENT_HELP.md +### Fixed +- Fixed `inspect undeclared` bug where pending status now takes precedence over identified status for the same component +### Changed +- Modified `inspect undeclared` view for `md` format to remove version information + +## [1.25.2] - 2025-06-18 +### Fixed +- Fixed errors when no versions are declared in scanner results for `inspect` subcommand +### Changed +- Prioritized licenses by source priority in `inspect copyleft` subcommand + +## [1.25.1] - 2025-06-12 +### Fixed +- Removed dependency components from the undeclared component list in the `inspect` subcommand. + +## [1.25.0] - 2025-06-10 +### Added +- Add `fh2` hash while fingerprinting mixed line ending files +### Modified +- Updated `inspect` debug/warning statements + +## [1.24.0] - 2025-05-28 +### Added +- Add `crypto` subcommand to retrieve cryptographic algorithms for the given components +- Add `crypto hints` subcommand to retrieve cryptographic hints for the given components +- Add `crypto versions-in-range` subcommand to retrieve cryptographic versions in range for the given components + +## [1.23.0] - 2025-04-24 +### Added +- Add `--origin` flag to `component provenance` subcommand to retrieve provenance using contributors origin +### Modified +- Update provenance GRPC stubs + +## [1.22.0] - 2025-04-23 +### Added +- Add `container-scan` subcommand to scan container images. +- Add `--container` flag to `dependency` subcommand to scan dependencies in container images. +### Modified +- Refactor CLI argument handling for output and format options. +### Fixed +- Fixed issue with wfp command where settings file was being loaded from the cwd instead of the scan root directory + +## [1.21.0] - 2025-03-27 +### Added +- Add folder-scan subcommand +- Add folder-hash subcommand +- Add AbstractPresenter class for presenting output in a given format +- Add several reusable helper functions for constructing config objects from CLI args + +## [1.20.6] - 2025-03-19 +### Added +- Added HTTP/gRPC generic headers feature using --header flag + +## [1.20.5] - 2025-03-13 +### Fixed +- Fixed timeout issue with dependency scan +### Added +- Improved documentation on spdxlite.py file +### Modified +- Added validation for license source on SPDXLite output format + +## [1.20.4] - 2025-03-05 +### Modified +- Updated Dockerfile to use Python 3.10-slim + +## [1.20.3] - 2025-03-03 +### Fixed +- Fixed cli.py typo + +## [1.20.2] - 2025-02-26 +### Fixed +- Fixed provenance command + +## [1.20.1] - 2025-02-18 +### Added +- Enhanced SPDX Lite report to achieve Telco compliance + +## [1.20.0] - 2025-02-02 +### Added +- Added support for component provenance reporting + - `scanoss-py component prov ...` + +## [1.19.6] - 2025-01-30 +### Added +- Omit settings file if it does not exist instead of throwing an error. +- Look settings file inside the folder being scanned instead of the cwd. + +## [1.19.5] - 2025-01-14 +### Added +- Add Docker image with SCANOSS user + +## [1.19.4] - 2025-01-08 +### Added +- Refactor on Jira Markdown output on inspect command + +## [1.19.3] - 2025-01-07 +### Added +- Add Jira Markdown output on inspect command +- This is useful for calls from integrations (i.e. Jenkins) + +## [1.19.2] - 2025-01-06 +### Added +- Add second container image `scanoss-py-base` with no `ENTRYPOINT` + - This is useful for calls from container pipelines (i.e. Jenkins) + +## [1.19.1] - 2025-01-06 +### Fixed +- Fixed undeclared components inspection + +## [1.19.0] - 2024-11-20 +### Fixed +- Check if legacy sbom file before post processing +### Added +- Use scanoss.json as default settings file if no argument is supplied +- Add —skip-settings-file flag +- Update scanoss settings schema to allow skipping specific folders, files, and extensions +- Add FileFilters class to handle filtering of files and folders based on settings + +## [1.18.1] - 2024-11-19 +### Added +- Added 'component' field in CycloneDX output + +## [1.18.0] - 2024-11-11 +### Fixed +- Fixed post processor being accesed if not set +### Added +- Added support for replace action when specifying a settings file +- Added replaced files as context to scan request +- Added sbom format flag to define status output for undeclared policy + +## [1.17.5] - 2024-11-12 +### Fixed +- Fix dependencies scan result structure + +## [1.17.4] - 2024-11-08 +### Fixed +- Fix backslashes in file paths on Windows + +## [1.17.3] - 2024-11-05 +### Fixed +- Fixed undeclared policy + + +## [1.17.2] - 2024-11-01 +### Fixed +- Fixed parsing of dependencies in Policy Checks +- Fixed legacy SBOM.json support +### Added +- Added supplier to SPDX packages +### Changed +- Changed undeclared summary output + +## [1.17.1] - 2024-10-24 +### Fixed +- Fixed policy summary output + +## [1.17.0] - 2024-10-23 +### Added +- Added inspect subcommand +- Inspect for copyleft licenses (`scanoss-py inspect copyleft -i scanoss-results.json`) +- Inspect for undeclared components (`scanoss-py inspect undeclared -i scanoss-results.json`) +### Fixed +- Fixed SPDX date format + +## [1.16.0] - 2024-10-08 +### Added +- Added the `metadata` field to the output in CycloneDX format, now including the fields `timestamp`, `tool vendor`, `tool` and `tool version` + +## [1.15.0] - 2024-09-17 +### Added +- Added Results sub-command: +- Get all results (`scanoss-py results /path/to/file`) +- Get filtered results (`scanoss-py results /path/to/file --match-type=file,snippet status=pending`) +- Get pending declarations (`scanoss-py results /path/to/file --has-pending`) +- Added `--settings` option to `scan` command to specify a settings file +- Specify settings file (`scanoss-py scan --settings /path/to/settings.json /path/to/file`) +- Added support for filtering dependencies based on development or production dependency scopes +- Added support for defining custom scopes to include or exclude dependencies with specified scope criteria + +## [1.14.0] - 2024-08-09 +### Added +- Added support for Python3.12 +- Module `pkg_resources` has been replaced with `importlib_resources` +- Added support for UTF-16 filenames + +## [1.13.0] - 2024-06-05 +### Added +- Added `scan` command option to specify a list of files (`--files`) to analyse + +## [1.12.3] - 2024-05-13 +### Fixed +- Fixed export issue when license details are missing (SPDX/CycloneDX) + +## [1.12.2] - 2024-04-15 +### Added +- Added [tagging workflow](.github/workflows/version-tag.yml) to aid release generation + +## [1.12.1] - 2024-04-12 +### Changed +- Removed '.whl' file extension from filtered extensions + +## [1.12.0] - 2024-03-26 +### Changed +- Updated free default URL to now point to `https://api.osskb.org` +- Updated premium default URL to now point to `https://api.scanoss.com` + +## [1.11.1] - 2024-03-18 +### Added +- Integrate CURL and jq + - Includes CURL and jq within the Docker image to facilitate seamless interactions with third-party integrations. + +## [1.11.0] - 2024-03-13 +### Added +- Added scan/wfp file filtering options + - Exclude files matching MD5 `--skip-md5` (repeat as needed) + - Strip code fragments using HPSM `--strip-hpsm` (repeat as needed) + - Strip code fragments using snippet IDs `--strip-snippet` (repeat as needed) + +## [1.10.0] - 2024-02-09 +### Added +- Added scan/wfp file filtering options + - Exclude file extensions `--skip-extension` (repeat as needed) + - Exclude folder `--skip-folder` (repeat as needed) + - Exclude files smaller than specified `--skip-size` +- Added `scan_files_with_options` SDK capability + - Enables a programmer to supply a specific list of files to scan + +## [1.9.0] - 2023-12-29 +### Added +- Added dependency file decoration option to scanning (`scan`) using `--dep` + - More details can be found in [CLIENT_HELP.md](CLIENT_HELP.md) + +## [1.8.0] - 2023-11-13 +### Added +- Added Component Decoration sub-command: + - Semgrep (`scanoss-py comp semgrep`) + +## [1.7.0] - 2023-09-15 +### Added +- Added Component Decoration sub-commands: + - Search (`scanoss-py comp search`) + - Versions (`scanoss-py comp versions`) + - Vulnerabilities (`scanoss-py comp vulns`) + +## [1.6.3] - 2023-08-22 +### Changed +- Changed default scan POST size to 32k +- Changed default scanning threads to 5 (and timeout to 180 seconds) +- Improved HPSM generation performance + +## [1.6.2] - 2023-08-11 +### Added +- Added `.woff2` to the list of file type to skip while scanning + +## [1.6.1] - 2023-07-06 +### Fixed +- Fixed issue with CSV dependency generation +- Increased `scanoss-winnowing` minimum requirement to match HPSM support + +## [1.6.0] - 2023-06-16 +### Added +- Added support for High Precision Snippet Matching (`--hpsm` or `-H`) while scanning + - `scanoss-py scan --hpsm ...` + +## [1.5.2] - 2023-06-13 +### Added +- Added retry limit option (`--retry`) while scanning + - `--retry 0` will fail immediately + +## [1.5.1] - 2023-04-21 +### Added +- Added support scanning/fingeprinting file contents from STDIN + - `cat test.py | scanoss-py scan --stdin test.py -o results.json` + - `cat test.py | scanoss-py wfp --stdin test.py -o fingers.wfp` + +## [1.5.0] - 2023-03-21 +### Added +- Added support for component cryptographic reporting + - `scanoss-py component crypto ...` + +## [1.4.2] - 2023-03-09 +### Fixed +- Fixed issue with custom certificate when scanning (--ca-cert) +### Added +- Added support to download full certificate chain with: + - `cert_download.sh` + - `scanoss-py utils cdl` + +## [1.4.0] - 2023-03-01 +### Added +- Added support for fast winnowing (15x improvement) thanks to a contribution from [tardyp](https://github.com/tardyp) + - This is enabled by a supporting package; [scanoss_winnowing](https://github.com/scanoss/scanoss-winnowing.py). + - It can be installed using: `pip3 install scanoss_winnowing` + - Or using: `pip3 install --upgrade scanoss[fast_winnowing]` + +## [1.3.7] - 2023-02-07 +### Added +- Upgrade to the latest protobuf and grpcio packages +- Added GH Actions for building + +## [1.3.6] - 2023-02-02 +### Added +- Added support for Proxy Auto-Config (--pac) and GRPC proxy (--grpc-proxy) + +## [1.3.5] - 2023-01-31 +### Added +- Added extra fields to CSV output (detected_url, detected_path) + +## [1.3.4] - 2023-01-16 +### Added +- Added User-Agent client/version to requests + +## [1.3.3] - 2023-01-06 +### Added +- Added support for handling 503 service unavailable responses +- Added latest SPDX license definitions (2.2.7) + +## [1.3.2] - 2022-12-28 +### Added +- Added `x-request-id` to all scanning requests +- Added bad_request error log file to aid debug +### Fixed +- Fixed issue when fingerprinting large files with a small POST (`--post-size`) + +## [1.3.1] - 2022-12-07 +### Added +- Added `utils cert-download` sub-command to help with the use of custom certificates + - Included a local certificate download script leveraging openssl too: [cert_download.sh](cert_download.sh) +- Added [documentation](CLIENT_HELP.md) to help with certificate and proxy configuration + +## [1.3.0] - 2022-12-02 +### Added +- Added support for proxy (--proxy) and certificates (--ca-certs) while scanning + - Certificates can also be supplied using environment variables: REQUESTS_CA_BUNDLE & GRPC_DEFAULT_SSL_ROOTS_FILE_PATH + - Proxies can be supplied using: grpc_proxy, https_proxy, http_proxy, HTTPS_PROXY, HTTP_PROXY +- Added snippet match fields to CSV output +- Added `convert` command to convert raw JSON reports into CSV, CycloneDX and SPDXLite +- Added `utils certloc` sub-command to print the location of Python's CA Cert file + - This is useful to know where to append custom certificates to if needed + +## [1.2.3] - 2022-11-22 +### Added +- Added Max Threaded scanning override env var (SCANOSS_MAX_ALLOWED_THREADS) + If the backend system can handle more than the current maximum (30), then set this env to that number + `export SCANOSS_MAX_ALLOWED_THREADS=40` + +## [1.2.2] - 2022-11-18 +### Added +- Added SSL cert error ignore option (--ignore-cert-errors) for REST calls + Custom certificates can be supplied using environment variables +- Added multi-platform Docker images (AMD64 & ARM64) + +## [1.2.1] - 2022-11-11 +### Added +- Added sub-command (file_count)to produce a file summary (extensions & size) into a CSV + +## [1.2.0] - 2022-11-08 +### Added +- Added vulnerability reporting to CycloneDX output +- Added obfuscation to fingerprinting (--obfuscate) +- Added obfuscation to scanning (--obfuscate) + +## [1.1.1] - 2022-10-19 +### Fixed +- Fixed issue with dependency parsing of yarn.lock files + +## [1.1.0] - 2022-10-12 +### Fixed +- Added LicenseRef info to SPDX Lite output +- Updated CycloneDX output format to support version 1.4 +### Added +- Added request id to gRPC requests + +## [1.0.6] - 2022-09-19 +### Added +- Added support for scancode 2.0 output format + +## [1.0.4] - 2022-09-07 +### Fixed +- Fixed spelling mistake in SPDX output +- Adjusted protobuf module requirements + +## [1.0.0] - 2022-07-22 ### Added -- Upcoming changes... +- Added support for CSV output (--format csv) +- Added documentDescribes to SPDXLite output ## [0.9.0] - 2022-06-09 ### Added @@ -92,3 +770,118 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [0.7.3]: https://github.com/scanoss/scanoss.py/compare/v0.7.2...v0.7.3 [0.7.4]: https://github.com/scanoss/scanoss.py/compare/v0.7.3...v0.7.4 [0.9.0]: https://github.com/scanoss/scanoss.py/compare/v0.7.4...v0.9.0 +[1.0.0]: https://github.com/scanoss/scanoss.py/compare/v0.9.0...v1.0.0 +[1.0.4]: https://github.com/scanoss/scanoss.py/compare/v1.0.0...v1.0.4 +[1.0.6]: https://github.com/scanoss/scanoss.py/compare/v1.0.4...v1.0.6 +[1.1.0]: https://github.com/scanoss/scanoss.py/compare/v1.0.6...v1.1.0 +[1.1.1]: https://github.com/scanoss/scanoss.py/compare/v1.1.0...v1.1.1 +[1.2.0]: https://github.com/scanoss/scanoss.py/compare/v1.1.1...v1.2.0 +[1.2.1]: https://github.com/scanoss/scanoss.py/compare/v1.2.0...v1.2.1 +[1.2.2]: https://github.com/scanoss/scanoss.py/compare/v1.2.1...v1.2.2 +[1.2.3]: https://github.com/scanoss/scanoss.py/compare/v1.2.2...v1.2.3 +[1.3.0]: https://github.com/scanoss/scanoss.py/compare/v1.2.3...v1.3.0 +[1.3.1]: https://github.com/scanoss/scanoss.py/compare/v1.3.0...v1.3.1 +[1.3.2]: https://github.com/scanoss/scanoss.py/compare/v1.3.1...v1.3.2 +[1.3.3]: https://github.com/scanoss/scanoss.py/compare/v1.3.2...v1.3.3 +[1.3.4]: https://github.com/scanoss/scanoss.py/compare/v1.3.3...v1.3.4 +[1.3.5]: https://github.com/scanoss/scanoss.py/compare/v1.3.4...v1.3.5 +[1.3.6]: https://github.com/scanoss/scanoss.py/compare/v1.3.5...v1.3.6 +[1.3.7]: https://github.com/scanoss/scanoss.py/compare/v1.3.6...v1.3.7 +[1.4.0]: https://github.com/scanoss/scanoss.py/compare/v1.3.7...v1.4.0 +[1.4.2]: https://github.com/scanoss/scanoss.py/compare/v1.4.0...v1.4.2 +[1.5.0]: https://github.com/scanoss/scanoss.py/compare/v1.4.2...v1.5.0 +[1.5.1]: https://github.com/scanoss/scanoss.py/compare/v1.5.0...v1.5.1 +[1.5.2]: https://github.com/scanoss/scanoss.py/compare/v1.5.1...v1.5.2 +[1.6.0]: https://github.com/scanoss/scanoss.py/compare/v1.5.2...v1.6.0 +[1.6.1]: https://github.com/scanoss/scanoss.py/compare/v1.6.0...v1.6.1 +[1.6.2]: https://github.com/scanoss/scanoss.py/compare/v1.6.1...v1.6.2 +[1.6.3]: https://github.com/scanoss/scanoss.py/compare/v1.6.2...v1.6.3 +[1.7.0]: https://github.com/scanoss/scanoss.py/compare/v1.6.3...v1.7.0 +[1.8.0]: https://github.com/scanoss/scanoss.py/compare/v1.7.0...v1.8.0 +[1.9.0]: https://github.com/scanoss/scanoss.py/compare/v1.8.0...v1.9.0 +[1.10.0]: https://github.com/scanoss/scanoss.py/compare/v1.9.0...v1.10.0 +[1.11.0]: https://github.com/scanoss/scanoss.py/compare/v1.10.0...v1.11.0 +[1.11.1]: https://github.com/scanoss/scanoss.py/compare/v1.11.0...v1.11.1 +[1.12.0]: https://github.com/scanoss/scanoss.py/compare/v1.11.1...v1.12.0 +[1.12.1]: https://github.com/scanoss/scanoss.py/compare/v1.12.0...v1.12.1 +[1.12.2]: https://github.com/scanoss/scanoss.py/compare/v1.12.1...v1.12.2 +[1.12.3]: https://github.com/scanoss/scanoss.py/compare/v1.12.2...v1.12.3 +[1.13.0]: https://github.com/scanoss/scanoss.py/compare/v1.12.3...v1.13.0 +[1.14.0]: https://github.com/scanoss/scanoss.py/compare/v1.13.0...v1.14.0 +[1.15.0]: https://github.com/scanoss/scanoss.py/compare/v1.14.0...v1.15.0 +[1.16.0]: https://github.com/scanoss/scanoss.py/compare/v1.15.0...v1.16.0 +[1.17.0]: https://github.com/scanoss/scanoss.py/compare/v1.16.0...v1.17.0 +[1.17.1]: https://github.com/scanoss/scanoss.py/compare/v1.17.0...v1.17.1 +[1.17.2]: https://github.com/scanoss/scanoss.py/compare/v1.17.1...v1.17.2 +[1.17.3]: https://github.com/scanoss/scanoss.py/compare/v1.17.2...v1.17.3 +[1.17.4]: https://github.com/scanoss/scanoss.py/compare/v1.17.3...v1.17.4 +[1.17.5]: https://github.com/scanoss/scanoss.py/compare/v1.17.4...v1.17.5 +[1.18.0]: https://github.com/scanoss/scanoss.py/compare/v1.17.5...v1.18.0 +[1.18.1]: https://github.com/scanoss/scanoss.py/compare/v1.18.0...v1.18.1 +[1.19.0]: https://github.com/scanoss/scanoss.py/compare/v1.18.1...v1.19.0 +[1.19.1]: https://github.com/scanoss/scanoss.py/compare/v1.19.0...v1.19.1 +[1.19.2]: https://github.com/scanoss/scanoss.py/compare/v1.19.1...v1.19.2 +[1.19.3]: https://github.com/scanoss/scanoss.py/compare/v1.19.2...v1.19.3 +[1.19.4]: https://github.com/scanoss/scanoss.py/compare/v1.19.3...v1.19.4 +[1.19.5]: https://github.com/scanoss/scanoss.py/compare/v1.19.4...v1.19.5 +[1.20.0]: https://github.com/scanoss/scanoss.py/compare/v1.19.5...v1.20.0 +[1.20.1]: https://github.com/scanoss/scanoss.py/compare/v1.20.0...v1.20.1 +[1.20.2]: https://github.com/scanoss/scanoss.py/compare/v1.20.1...v1.20.2 +[1.20.3]: https://github.com/scanoss/scanoss.py/compare/v1.20.2...v1.20.3 +[1.20.4]: https://github.com/scanoss/scanoss.py/compare/v1.20.3...v1.20.4 +[1.20.5]: https://github.com/scanoss/scanoss.py/compare/v1.20.4...v1.20.5 +[1.20.6]: https://github.com/scanoss/scanoss.py/compare/v1.20.5...v1.20.6 +[1.21.0]: https://github.com/scanoss/scanoss.py/compare/v1.20.6...v1.21.0 +[1.22.0]: https://github.com/scanoss/scanoss.py/compare/v1.21.0...v1.22.0 +[1.23.0]: https://github.com/scanoss/scanoss.py/compare/v1.22.0...v1.23.0 +[1.24.0]: https://github.com/scanoss/scanoss.py/compare/v1.23.0...v1.24.0 +[1.25.0]: https://github.com/scanoss/scanoss.py/compare/v1.24.0...v1.25.0 +[1.25.1]: https://github.com/scanoss/scanoss.py/compare/v1.25.0...v1.25.1 +[1.25.2]: https://github.com/scanoss/scanoss.py/compare/v1.25.1...v1.25.2 +[1.26.0]: https://github.com/scanoss/scanoss.py/compare/v1.25.2...v1.26.0 +[1.26.1]: https://github.com/scanoss/scanoss.py/compare/v1.26.0...v1.26.1 +[1.26.2]: https://github.com/scanoss/scanoss.py/compare/v1.26.1...v1.26.2 +[1.26.3]: https://github.com/scanoss/scanoss.py/compare/v1.26.2...v1.26.3 +[1.27.0]: https://github.com/scanoss/scanoss.py/compare/v1.26.3...v1.27.0 +[1.27.1]: https://github.com/scanoss/scanoss.py/compare/v1.27.0...v1.27.1 +[1.28.0]: https://github.com/scanoss/scanoss.py/compare/v1.27.1...v1.28.0 +[1.28.1]: https://github.com/scanoss/scanoss.py/compare/v1.28.0...v1.28.1 +[1.28.2]: https://github.com/scanoss/scanoss.py/compare/v1.28.1...v1.28.2 +[1.29.0]: https://github.com/scanoss/scanoss.py/compare/v1.28.2...v1.29.0 +[1.30.0]: https://github.com/scanoss/scanoss.py/compare/v1.29.0...v1.30.0 +[1.31.0]: https://github.com/scanoss/scanoss.py/compare/v1.30.0...v1.31.0 +[1.31.1]: https://github.com/scanoss/scanoss.py/compare/v1.31.0...v1.31.1 +[1.31.2]: https://github.com/scanoss/scanoss.py/compare/v1.31.1...v1.31.2 +[1.31.3]: https://github.com/scanoss/scanoss.py/compare/v1.31.2...v1.31.3 +[1.31.4]: https://github.com/scanoss/scanoss.py/compare/v1.31.3...v1.31.4 +[1.31.5]: https://github.com/scanoss/scanoss.py/compare/v1.31.4...v1.31.5 +[1.32.0]: https://github.com/scanoss/scanoss.py/compare/v1.31.5...v1.32.0 +[1.33.0]: https://github.com/scanoss/scanoss.py/compare/v1.32.0...v1.33.0 +[1.34.0]: https://github.com/scanoss/scanoss.py/compare/v1.33.0...v1.34.0 +[1.35.0]: https://github.com/scanoss/scanoss.py/compare/v1.34.0...v1.35.0 +[1.36.0]: https://github.com/scanoss/scanoss.py/compare/v1.35.0...v1.36.0 +[1.37.0]: https://github.com/scanoss/scanoss.py/compare/v1.36.0...v1.37.0 +[1.37.1]: https://github.com/scanoss/scanoss.py/compare/v1.37.0...v1.37.1 +[1.38.0]: https://github.com/scanoss/scanoss.py/compare/v1.37.1...v1.38.0 +[1.39.0]: https://github.com/scanoss/scanoss.py/compare/v1.38.0...v1.39.0 +[1.40.0]: https://github.com/scanoss/scanoss.py/compare/v1.39.0...v1.40.0 +[1.40.1]: https://github.com/scanoss/scanoss.py/compare/v1.40.0...v1.40.1 +[1.41.0]: https://github.com/scanoss/scanoss.py/compare/v1.40.1...v1.41.0 +[1.41.1]: https://github.com/scanoss/scanoss.py/compare/v1.41.0...v1.41.1 +[1.42.0]: https://github.com/scanoss/scanoss.py/compare/v1.41.1...v1.42.0 +[1.43.0]: https://github.com/scanoss/scanoss.py/compare/v1.42.0...v1.43.0 +[1.43.1]: https://github.com/scanoss/scanoss.py/compare/v1.43.0...v1.43.1 +[1.44.0]: https://github.com/scanoss/scanoss.py/compare/v1.43.1...v1.44.0 +[1.45.0]: https://github.com/scanoss/scanoss.py/compare/v1.44.0...v1.45.0 +[1.45.1]: https://github.com/scanoss/scanoss.py/compare/v1.45.0...v1.45.1 +[1.46.0]: https://github.com/scanoss/scanoss.py/compare/v1.45.1...v1.46.0 +[1.47.0]: https://github.com/scanoss/scanoss.py/compare/v1.46.0...v1.47.0 +[1.48.0]: https://github.com/scanoss/scanoss.py/compare/v1.47.0...v1.48.0 +[1.49.0]: https://github.com/scanoss/scanoss.py/compare/v1.48.0...v1.49.0 +[1.49.1]: https://github.com/scanoss/scanoss.py/compare/v1.49.0...v1.49.1 +[1.50.0]: https://github.com/scanoss/scanoss.py/compare/v1.49.1...v1.50.0 +[1.50.1]: https://github.com/scanoss/scanoss.py/compare/v1.50.0...v1.50.1 +[1.51.0]: https://github.com/scanoss/scanoss.py/compare/v1.50.1...v1.51.0 +[1.51.1]: https://github.com/scanoss/scanoss.py/compare/v1.51.0...v1.51.1 +[1.52.0]: https://github.com/scanoss/scanoss.py/compare/v1.51.1...v1.52.0 +[1.52.0]: https://github.com/scanoss/scanoss.py/compare/v1.51.1...v1.52.1 diff --git a/CLIENT_HELP.md b/CLIENT_HELP.md new file mode 100644 index 00000000..e23a44a7 --- /dev/null +++ b/CLIENT_HELP.md @@ -0,0 +1,759 @@ +# SCANOSS Client Usage Help +This file contains useful tips/tricks for getting the most out of the SCANOSS platform using the Python client/SDK. + +## Installation +### Externally Managed Environments Error +If installing on Ubuntu 2023.04, Fedora 38, Debian 11, etc. a few additional steps are required before installing `scanoss-py`. More details can be found [here](https://itsfoss.com/externally-managed-environment/). + +The recommended method is to install `pipx` and use it to install `scanoss-py`: +```bash +sudo apt install pipx +pipx ensurepath +``` + +This will install the `pipx` package manager, which can then be used to install `scanoss-py`: +```bash +pipx install scanoss[fast_winnowing] +``` +This will install the `scanoss-py` app in a separate virtual environment and create a link to the local path for execution. + +## Certificate Management +The SCANOSS SaaS platform runs over HTTPS with publicly signed SSL certificates. +However, on-premise installations, or those with a proxy in the middle might be leveraging self-signed versions. + +This can cause issues for the SCANOSS clients. + +### Certificate Download +In order to connect to a self-signed endpoint, it's necessary to download that cert and add it to the trust store for the client. +The following is an OpenSSL-based command script which can produce this file: +```shell +cert_download.sh -n +``` +Simply pass in the hostname `-n scanoss.com` and optionally the port `-p 8443` (defaults to `443`) and it will produce a PEM file called `scanoss.com.pem`. + +The `scanoss-py` CLI also supports certificate download using this command: +```shell +scanoss-py utils cdl -n scanoss.com -o scanoss-com.pem +``` + +It is also possible to download the certificate using a web browser, for example FireFox. Simply browse to the site, view the certificate and choose to download. + +### Use Custom Certificate with CLI +There are a number of ways to leverage this custom certificate from the `scanoss-py` CLI. +- Environment Variables +- Command Line Options +- Appending to the default certificates + +#### Custom Certificate with Env Vars +The `scanoss-py` CLI uses two communication methods; REST & gRPC and as such requires two env vars to be set if following this method. +- REST - Use `REQUESTS_CA_BUNDLE` + - `export REQUESTS_CA_BUNDLE=/path/to/cert.pem` +- gRPC - Use `GRPC_DEFAULT_SSL_ROOTS_FILE_PATH` + - `export GRPC_DEFAULT_SSL_ROOTS_FILE_PATH=/path/to/cert.pem` + +#### Custom Certificate with CLI Options +The `scanoss-py` CLI has a `--ca-cert` option to allow the specification of a custom certificate file to be used when communicating over REST. +Simply set it using: +```shell +scanoss-py scan --ca-cert scanoss-com.pem -o results.json . +``` +Alternative API Urls can also be configured (if necessary) using `--apiurl`. + +#### Custom Certificate appended to Defaults +It is also possible to append this custom certificate to the default certificate list used by `scanoss-py`. +This file location can be determined by using: +```shell +scanoss-py utils cl +``` +The resulting certificate file name can then be opened and the custom certificate appended to the end. +For example: +```shell +cat scanoss-com.pem >> /usr/local/lib/python3.10/site-packages/certifi/cacert.pem +``` + +## Proxy Configuration +The SCANOSS clients can be configured to work with proxies. There are a number of ways to achieve this: + +- Environment Variables +- Command Line Options + +### Proxy Env Vars +There are a number of environment variables that can be specified to force the `scanoss-py` command to route calls via proxy. + +- REST - `https_proxy`, `http_proxy`, `HTTPS_PROXY`, `HTTP_PROXY` + +Set the variable as follows: `export https_proxy="http://:"` + +The REST client supports both lowercase & uppercase proxy names. + +### Proxy CLI Options +The proxy for REST based calls can also be configured directly on the `scanoss-py` commandline using `--proxy`. For example: +```shell +scanoss-py scan --proxy "http://:" -o results.json . +``` + +### Proxy Auto-Config CLI Options +The `scanoss-py` CLI also supports Proxy Auto-Config (PAC) when scanning using the `--pac` command option. + +It supports three options: +* auto - check the system for a PAC configuration + * `scanoss-py scan --pac auto -o results.json .` +* file - load a local PAC file + * `scanoss-py scan --pac file://proxy.pac -o results.json .` +* url - download a specific PAC file + * `scanoss-py scan --pac https://path.to/proxy.pac -o results.json .` + +### PAC File Evaluation +The `scanoss-py` CLI provides a utility command to help identify if traffic to the SCANOSS services is required over a proxy or not. + +Simply run the following commands find out: +* auto + * `scanoss-py utils pac-proxy --pac auto --url https://api.osskb.org` +* file + * `scanoss-py utils pac-proxy --pac file://proxy.pac --url https://api.osskb.org` +* url + * `scanoss-py utils pac-proxy --pac https://path.to/proxy.pac --url https://api.osskb.org` + +## GRPCIO Library installation for Apple Silicon (before 1.5.3) +Versions of [grpcio](https://pypi.org/project/grpcio) prior to `1.5.3` did not contain a binary wheel for Apple Silicon. + +[Pietro De Nicolao](https://github.com/pietrodn) has kindly created a [GitHub repo](https://github.com/pietrodn/grpcio-mac-arm-build) to build the M1/M2 compatible wheels. +Simply browse to the [releases](https://github.com/pietrodn/grpcio-mac-arm-build/releases) area, choose the desired release version and install the wheel matching your python version: +```bash +pip3 install --upgrade https://github.com/pietrodn/grpcio-mac-arm-build/releases/download/1.51.1/grpcio-1.51.1-cp39-cp39-macosx_11_0_arm64.whl +``` + +This command above will install `grpcio` `1.5.1` for Python `3.9`. To install for `3.10` simply replace the `cp39` with `cp310`. + +## Debug Mode +The SCANOSS CLI supports debug mode to provide additional diagnostic information during command execution. This is useful for troubleshooting issues or understanding the internal operations of the CLI. + +There are two ways to enable debug mode: + +### Debug Mode via Environment Variable +Set the `SCANOSS_DEBUG` environment variable to `true`: +```bash +export SCANOSS_DEBUG=true +scanoss-py scan -o results.json src +``` + +This method is particularly useful when you want debug output enabled for multiple consecutive commands without having to add the flag each time. + +### Debug Mode via Command Line Flag +Use the `-d` or `--debug` flag with any command: +```bash +scanoss-py scan -d -o results.json src +``` + +**Note:** The command line flag will override the environment variable setting if both are present. + +## Command Execution +There are multiple commands (and sub commands) available through `scanoss-py`. +Detailed help is available for all directly from the CLI itself: +```bash +scanoss-py --help +scanoss-py scan --help +scanoss-py comp +scanoss-py comp vulns --help +scanoss-py utils +scanoss-py inspect +``` + +### Fingerprint a project folder +The following command provides the capability to fingerprint (generate WFPs) for a given file/folder: +```bash +scanoss-py wfp --help +``` +The following command fingerprints the `src` folder and writes the output to `src-fingers.wfp`: +```bash +scanoss-py wfp -o src-fingers.wfp src +``` + +This fingerprint (WFP) can then be sent to the SCANOSS engine using the scanning command: +```bash +scanoss-py scan -w src-fingers.wfp -o scan-results.json +``` + +### Dependency file parsing +The dependency files of a project can be fingerprinted/parsed using the `dep` command: +```bash +scanoss-py dep -o src-deps.json src +``` + +You can also analyze dependencies from a container image using the `--container` flag: +```bash +scanoss-py dep --container ubuntu:latest -o container-deps.json +``` + +This parsed dependency file can then be sent to the SCANOSS for decoration using the scanning command: +```bash +scanoss-py scan --dep src-deps.json --dependencies-only -o scan-results.json +``` + +It is possible to combine a WFP & Dependency file into a single scan also: +```bash +scanoss-py scan -w src-fingers.wfp --dep src-deps.json -o scan-results.json +``` + +### Scan a project folder +The following command provides the capability to scan a given file/folder: +```bash +scanoss-py scan --help +``` + +The following command scans the `src` folder and writes the output to `scan-results.json`: +```bash +scanoss-py scan -o scan-results.json src +``` + +### Scan a project folder with dependencies +The following command scans the `src` folder for file, snippet & dependency matches, writing the output to `scan-results.json`: +```bash +scanoss-py scan -o scan-results.json -D src +``` + +### Scan a project folder with filtered dependency scopes +The following command scans the src folder for files, code snippets, and dependencies, specifically targeting development dependencies: +The available flags for filtering dependency scopes are **__dev__** for development dependencies or **__prod__** for production dependencies: +```bash +scanoss-py scan -D src --dep-scope dev +``` + +### Scan a project folder including dependencies with declared scopes +The following command scans the src folder for files, code snippets, and dependencies, allowing you to specify which dependency scopes to include. +In this example, the scan targets the dependencies and install scopes: +```bash +scanoss-py scan -D src --dep-scope-inc dependencies,install +``` + +### Scan a project folder excluding dependencies with declared scopes +The following command scans the src folder for files, code snippets, and dependencies, allowing you to specify which dependency scopes to exclude. +In this example, the scan targets dependencies but excludes those within the install scope: +```bash +scanoss-py scan -D src --dep-scope-exc install +``` + +### Scan a project folder skipping files and snippets +The following command scans the `src` folder writing the output to `scan-results.json` skipping the following: +- MD5 file `37f7cd1e657aa3c30ece35995b4c59e5` +- Header files `.h` +- Files smaller than 512 byes +- Files inside folder `internal` +- Snippets matching `d5e54c33,b03faabe` +```bash +scanoss-py scan -o scan-results.json -5 37f7cd1e657aa3c30ece35995b4c59e5 -E '.h' -Z 512 -O internal -N 'd5e54c33,b03faabe' src +``` + +### Scan with custom headers +Scan with custom headers. This example scans the `src` folder and sends a custom API key header with the request: +```bash +scanoss-py scan -o scan-results.json src -hdr "x-api-key:12345" +``` +Multiple Headers: You can specify any number of custom headers by repeating the -hdr option: +```bash +scanoss-py scan src -hdr "x-api-key:12345" -hdr "Authorization: Bearer " +``` + +### Scan with Snippet Tuning Options +The following flags allow you to fine-tune snippet matching behavior during scanning. These options can be set via command line flags or in the `scanoss.json` configuration file under `settings.file_snippet`. + +#### Set minimum snippet hits +Require at least 5 snippet hits for a match. A value of 0 defers to server configuration: +```bash +scanoss-py scan -o scan-results.json --min-snippet-hits 5 src +``` +**scanoss.json configuration:** +```json +{ + "settings": { + "file_snippet": { + "min_snippet_hits": 5 + } + } +} +``` + +#### Set minimum snippet lines +Require at least 3 snippet lines for a match. A value of 0 defers to server configuration: +```bash +scanoss-py scan -o scan-results.json --min-snippet-lines 3 src +``` +**scanoss.json configuration:** +```json +{ + "settings": { + "file_snippet": { + "min_snippet_lines": 3 + } + } +} +``` + +#### Enable or disable ranking +Enable ranking to prioritize results: +```bash +scanoss-py scan -o scan-results.json --ranking true src +``` +Disable ranking: +```bash +scanoss-py scan -o scan-results.json --ranking false src +``` +**scanoss.json configuration:** +```json +{ + "settings": { + "file_snippet": { + "ranking_enabled": true + } + } +} +``` + +#### Set ranking threshold +Set the ranking threshold to 5 (valid range: -1 to 10). A value of -1 defers to server configuration: +```bash +scanoss-py scan -o scan-results.json --ranking-threshold=5 src +``` +Note: Use `=` syntax for negative values: `--ranking-threshold=-1` + +**scanoss.json configuration:** +```json +{ + "settings": { + "file_snippet": { + "ranking_threshold": 5 + } + } +} +``` + +#### Honour file extensions +Control whether file extensions are considered during matching: +```bash +scanoss-py scan -o scan-results.json --honour-file-exts true src +``` +**scanoss.json configuration:** +```json +{ + "settings": { + "file_snippet": { + "honour_file_exts": true + } + } +} +``` + +#### Skip headers +Skip license headers, comments and imports at the beginning of files during snippet scanning: +```bash +scanoss-py scan -o scan-results.json --skip-headers src +``` +**scanoss.json configuration:** +```json +{ + "settings": { + "file_snippet": { + "skip_headers": true + } + } +} +``` + +#### Skip headers limit +Set the maximum number of lines to skip when filtering headers (requires `--skip-headers`): +```bash +scanoss-py scan -o scan-results.json --skip-headers --skip-headers-limit 50 src +``` +**scanoss.json configuration:** +```json +{ + "settings": { + "file_snippet": { + "skip_headers": true, + "skip_headers_limit": 50 + } + } +} +``` + +#### Combine multiple tuning options +You can combine multiple tuning options in a single scan: +```bash +scanoss-py scan -o scan-results.json --min-snippet-hits 5 --min-snippet-lines 3 --ranking true --ranking-threshold=5 src +``` +**scanoss.json configuration:** +```json +{ + "settings": { + "file_snippet": { + "min_snippet_hits": 5, + "min_snippet_lines": 3, + "ranking_enabled": true, + "ranking_threshold": 5, + "honour_file_exts": true, + "skip_headers": true, + "skip_headers_limit": 50 + } + } +} +``` + +### Converting RAW results into other formats +The following command provides the capability to convert the RAW scan results from a SCANOSS scan into multiple different formats, including CycloneDX, SPDX Lite, CSV and GitLab Code Quality Report. +For the full set of formats, please run: +```bash +scanoss-py cnv --help +``` + +The following command converts `scan-results.json` to SPDX Lite: +```bash +scanoss-py cnv --input scan-results.json --format spdxlite --output scan-results-spdxlite.json +``` + +The following command converts `scan-results.json` to GitLab Code Quality Report: +```bash +scanoss-py cnv --input scan-results.json --format glc-codequality --output gl-code-quality-report.json +``` + + +### Component Commands +The `component` command has a suite of sub-commands designed to operate on OSS components. For example: +* Vulnerabilities (`vulns`) +* Search (`search`) +* Version Details (`versions`) +* Cryptography (`crypto`) +* Provenance (`provenance`) +* Licenses (`licenses`) +* Status (`status`) + +For the latest list of sub-commands, please run: +```bash +scanoss-py comp --help +``` + +All component sub-commands support custom headers using the `-hdr` option: +```bash +scanoss-py comp search "jquery" -hdr "x-api-key:12345" +scanoss-py comp vulns "jquery@3.6.0" -hdr "x-api-key:12345" -hdr "custom-header:value" +scanoss-py comp crypto --purl "pkg:github/madler/pigz" -header "x-api-key:12345" + +#### Component Vulnerabilities +The following command provides the capability to search the SCANOSS KB for component vulnerabilities: +```bash +scanoss-py comp vulns -p "pkg:github/unoconv/unoconv" +``` +It is possible to supply multiple PURLs by repeating the `-p pkg` option, or providing a purl input file `-i purl-input.json` ([for example](tests/data/purl-input.json)): +```bash +scanoss-py comp vulns -i purl-input.json -o vulnernable-comps.json +``` + +#### Component Search +The following command provides the capability to search the SCANOSS KB for an Open Source component: +```bash +scanoss-py comp search --key $SC_API_KEY -s "unoconv" +``` +This command will search through different combinations to retrieve a proposed list of components (i.e. vendor/component, component, vendor, purl). + +It is also possible to search by component and vendor, while restricting the package type: +```bash +scanoss-py comp search --key $SC_API_KEY -c unoconv -v unoconv -p github +``` +**Note:** This sub-command requires a subscription to SCANOSS premium data. + +#### Component Versions +The following command provides the capability to search the SCANOSS KB for versions of a specified component PURL: +```bash +scanoss-py comp versions --key $SC_API_KEY -p "pkg:github/unoconv/unoconv" +``` +**Note:** This sub-command requires a subscription to SCANOSS premium data. + +#### Cryptographic Algorithms +The following command provides the capability to search the SCANOSS KB for any cryptographic algorithms detected in a specified component PURL: +```bash +scanoss-py comp crypto --key $SC_API_KEY -p "pkg:github/unoconv/unoconv" +``` +It is possible to supply multiple PURLs by repeating the `-p pkg` option, or providing a purl input file `-i purl-input.json` ([for example](tests/data/purl-input.json)): +```bash +scanoss-py comp crypto --key $SC_API_KEY -i purl-input.json -o crypto-components.json +``` +**Note:** This sub-command requires a subscription to SCANOSS premium data. + +#### Semgrep Issues/Findings +The following command provides the capability to search the SCANOSS KB for any semgrep issues detected in a specified component PURL: +```bash +scanoss-py comp semgrep --key $SC_API_KEY -p "pkg:github/spring-projects/spring-data-jpa" +``` +It is possible to supply multiple PURLs by repeating the `-p pkg` option, or providing a purl input file `-i purl-input.json` ([for example](tests/data/purl-input.json)): +```bash +scanoss-py comp semgrep --key $SC_API_KEY -i purl-input.json -o semgrep-issues.json +``` +**Note:** This sub-command requires a subscription to SCANOSS premium data. + +#### Component Provenance +The following command provides the capability to search the SCANOSS KB for component Provenance: +```bash +scanoss-py comp prov -p "pkg:github/unoconv/unoconv" +``` +It is possible to supply multiple PURLs by repeating the `-p pkg` option, or providing a purl input file `-i purl-input.json` ([for example](tests/data/purl-input.json)): +```bash +scanoss-py comp prov -i purl-input.json -o provenance.json +``` + +#### Component Provenance Using Contributors Origin +The following command provides the capability to search the SCANOSS KB for component Provenance using contributors origin: +```bash +scanoss-py comp prov -p "pkg:github/unoconv/unoconv" --origin +``` + +#### Component Licenses +The following command provides the capability to search the SCANOSS KB for licenses for Open Source components: +```bash +scanoss-py comp licenses -p "pkg:github/jquery/jquery" -p "pkg:npm/express" +``` +It is possible to supply multiple PURLs by repeating the `-p pkg` option, or providing a purl input file `-i purl-input.json` ([for example](tests/data/purl-input.json)): +```bash +scanoss-py comp licenses -i purl-input.json -o component-licenses.json +``` + +The licenses command also supports CycloneDX (CDX) input files. You can provide a CycloneDX SBOM file and retrieve license information for all components: +```bash +scanoss-py comp licenses -i cyclonedx-sbom.json -o component-licenses.json +``` + +#### Component Status +The following command provides the capability to search the SCANOSS KB for development status information of Open Source components: +```bash +scanoss-py comp status -p "pkg:npm/react@17.0.2" +``` +It is possible to supply multiple PURLs by repeating the `-p pkg` option, or providing a purl input file `-i purl-input.json` ([for example](tests/data/purl-input.json)): +```bash +scanoss-py comp status -i purl-input.json -o component-status.json +``` + +The status command also supports CycloneDX (CDX) input files. You can provide a CycloneDX SBOM file and retrieve status information for all components: +```bash +scanoss-py comp status -i cyclonedx-sbom.json -o component-status.json +``` + +The component status provides information about: +- **Component status**: Overall status of the component (active, inactive, deprecated) +- **Repository status**: Current status of the component's repository +- **First indexed date**: When the component was first indexed in SCANOSS KB +- **Last indexed date**: Most recent indexing date +- **Version status**: Status specific to the requested version +- **Indexed date**: When the specific version was indexed + +### CDX Input Support for Component Commands +Several component commands now support CycloneDX (CDX) input files. This allows you to analyze components from existing SBOM files: + +**Supported commands with CDX input:** +- `comp vulns` - Analyze vulnerabilities from CDX file +- `comp licenses` - Retrieve licenses from CDX file +- `comp crypto` - Detect cryptographic algorithms from CDX file +- `comp semgrep` - Find semgrep issues from CDX file +- `comp status` - Retrieve development status from CDX file + +**Example using CDX input:** +```bash +# Analyze vulnerabilities from a CycloneDX SBOM +scanoss-py comp vulns -i sbom.cdx.json -o vulnerabilities.json + +# Get licenses for all components in a CycloneDX SBOM +scanoss-py comp licenses -i sbom.cdx.json -o licenses.json + +# Get status information for all components in a CycloneDX SBOM +scanoss-py comp status -i sbom.cdx.json -o status.json + +# Detect cryptographic usage from CDX +scanoss-py comp crypto -i sbom.cdx.json -o crypto-findings.json +``` + +The CDX input file is automatically validated to ensure it's a valid CycloneDX format before processing. + + +### Results Commands +The `results` command provides the capability to operate on scan results. For example: + +The following command gets the pending results from a scan: +```bash +scanoss-py results results.json --has-pending +``` + +You can indicate the output format and an output file: +```bash +scanoss-py results results.json --format json --output results-output.json +``` + +You can also filter the results by either status or match type: +```bash +scanoss-py results results.json --status pending --match-type file +``` + +You can provide a comma separated list of statuses or match types: +```bash +scanoss-py results results.json --status pending,identified --match-type file,snippet +``` + + +### Inspect Commands +The `inspect` command has a suite of sub-commands designed to inspect the results.json. +Details, such as license compliance or component declarations, can be examined. + +For example: +* Copyleft (`copyleft`) +* Undeclared Components (`undeclared`) +* License Summary (`license-summary`) +* Component Summary (`component-summary`) +* Dependency Track project violations (`dependency-track project-violations`) +* GitLab Components Match Summary (`gitlab matches`) + +For the latest list of sub-commands, please run: +```bash +scanoss-py insp --help +``` +#### Inspect Copyleft +The following command can be used to inspect for copyleft licenses. +If no output or status flag is defined, details are exposed via stdout and the summary is provided via stderr. +Default format 'json' +```bash +scanoss-py insp copyleft -i scan-results.json +``` + +#### Inspect for copyleft licenses and save results +The following command can be used to inspect for copyleft licenses and save the results. +Default output format 'json'. +```bash +scanoss-py insp copyleft -i scan-results.json --status status.md --output copyleft.json +``` + +#### Inspect for copyleft licenses and save results in Markdown format +The following command can be used to inspect for copyleft licenses and save the results in Markdown format. +```bash +scanoss-py insp copyleft -i scan-results.json --status status.md --output copyleft.md --format md +``` + +#### Inspect for undeclared components +The following command can be used to inspect for undeclared components. +If no output or status flag is defined, details are exposed via stdout and the summary is provided via stderr. +Default output format 'json'. +```bash +scanoss-py insp undeclared -i scan-results.json +``` + +#### Inspect for undeclared components and save results +The following command can be used to inspect for undeclared components and save the results. +Default output format 'json'. +```bash +scanoss-py insp undeclared -i scan-results.json --status undeclared-status.md --output undeclared.json +``` + +#### Inspect for undeclared components and save results in Markdown format +The following command can be used to inspect for undeclared components and save the results in Markdown format. +```bash +scanoss-py insp undeclared -i scan-results.json --status undeclared-status.md --output undeclared.json --format md +``` + +#### Inspect for undeclared components and save results in Markdown format and show status output as sbom.json (legacy) +The following command can be used to inspect for undeclared components and save the results in Markdown format. +```bash +scanoss-py insp undeclared -i scan-results.json --status undeclared-status.md --output undeclared.json --format md --sbom-format legacy +``` + +#### Inspect for undeclared components and save results in Jira Markdown format. +The following command can be used to inspect for undeclared components and save the results in Jira Markdown format. +```bash +scanoss-py insp undeclared -i scan-results.json --output undeclared-summary.jiramd --status undeclared-status.jiramd --format jira_md +``` + +#### Inspect for copyleft licenses and save results in Jira Markdown format. +The following command can be used to inspect for undeclared components and save the results in Jira Markdown format. +```bash +scanoss-py insp copyleft -i scan-results.json --output copyleft-summary.jiramd --status copyleft-status.jiramd --format jira_md +``` + +#### Inspect for license summary from scan results +The following command can be used to get a license summary from scan results. +```bash +scanoss-py insp license-summary -i scan-results.json --output license-summary.json +``` + +Example with an output file: + +```bash +scanoss-py insp license-summary -i scan-results.json --output license-summary.txt +``` + +#### Inspect for license summary from scan results with custom copyleft licenses +The following command can be used to get a license summary from scan results. + +Example including a license to the default list +```bash +scanoss-py insp license-summary -i scan-results.json --output license-summary.json --include Zlib,MIT +``` + +Example excluding a license from the default list +```bash +scanoss-py insp license-summary -i scan-results.json --output license-summary.txt --exclude GPL-2.0-only +``` + +Example getting only explicit declared licenses +```bash +scanoss-py insp license-summary -i scan-results.json --output license-summary.json --explicit Zlib +``` + +#### Inspect for component summary from scan results +The following command can be used to get a component summary from scan results and save the output. +```bash +scanoss-py insp component-summary -i scan-results.json +``` +Example with an output file: +```bash +scanoss-py insp component-summary -i scan-results.json --output component-summary.json +``` + +#### Inspect Dependency Track project violations Markdown output +The following command can be used to retrieve project violations from Dependency Track in Markdown format. + +**Note:** The upload token is optional. It is used to check the project processing status. If no token is provided, the latest project violations will be retrieved without waiting for project processing to complete. + +Example with project id: +```bash +scanoss-py inspect dt project-violations --dt-upload-token --dt-url --dt-projectid --dt-apikey --format md --output project-violations.md +``` +Example with project name and version: +```bash +scanoss-py inspect dt project-violations --dt-upload-token --dt-url --dt-projectname --dt-projectversion --dt-apikey --format md --output project-violations.md +``` + +#### Inspect GitLab Component Match Summary Markdown Output +The following command can be used to generate a component match summary in Markdown format for GitLab: +```bash +scanoss-py inspect gitlab matches --input -lpr --output gitlab-component-match-summary.md +``` + +### Folder-Scan a Project Folder + +The new `folder-scan` subcommand performs a comprehensive scan on an entire directory by recursively processing files to generate folder-level fingerprints. It computes CRC64 hashes and simhash values to detect directory-level similarities, which is especially useful for comparing large code bases or detecting duplicate folder structures. + +**Usage:** +```shell +scanoss-py folder-scan /path/to/folder -o folder-scan-results.json +``` + +**Options:** +- `--rank-threshold`: Filter results to only show those with rank value at or below this threshold (e.g., `--rank-threshold 3` returns results with rank 1, 2, or 3). Lower rank values indicate higher quality matches. +- `--format`: Result output format (json or cyclonedx, default: json) + +**Example with rank threshold:** +```shell +scanoss-py folder-scan /path/to/folder --rank-threshold 3 -o folder-scan-results.json +``` + +### Container-Scan a Docker Image + +The `container-scan` subcommand allows you to scan Docker container images for dependencies. This command extracts and analyzes dependencies from container images, helping you identify open source components within containerized applications. + +**Usage:** +```shell +scanoss-py container-scan image_name:tag -o container-scan-results.json +``` \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a028d2d..f6113224 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,8 +21,6 @@ Want to submit a pull request? Great! But please follow some basic rules: - If you are changing a source file please make sure that you only include in the changeset the lines changed by you (beware of your editor reformatting the file) - If you are adding functionality, please write a unit test. -When reviewing your pull request, we will follow a checklist similar to this one: https://gist.github.com/audreyr/4feef90445b9680475f2 - ### Licensing The SCANOSS Platform is released under the GPL-2.0 license. If you wish to contribute, you must accept that you are aware of the license under which the project is released, and that your contribution will be released under the same license. Sometimes the GPL-2.0 license is incompatible with other licenses chosen by other projects. Therefore, you must accept that your contribution can also be released under the MIT license, which is the license we choose for those situations. Unless you expressly request otherwise, we may use your name, email address, username or URL for your attribution notice text. The submission of your contribution implies that you agree with these licensing terms. diff --git a/Dockerfile b/Dockerfile index b11d7812..2005bc3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,40 +1,87 @@ -FROM python:3.8-slim-buster as base +FROM --platform=$BUILDPLATFORM python:3.10-slim-bookworm AS base LABEL maintainer="SCANOSS " +LABEL org.opencontainers.image.source=https://github.com/scanoss/scanoss.py +LABEL org.opencontainers.image.description="SCANOSS Python CLI Container" +LABEL org.opencontainers.image.licenses=MIT -FROM base as builder +# Compile and install all the necessary python requirements +FROM base AS builder # Setup the required build tooling RUN apt-get update \ - && apt-get install -y --no-install-recommends build-essential gcc \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ + && apt-get install -y --no-install-recommends build-essential gcc libicu-dev pkg-config \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Create and activate virtual environment +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" -RUN mkdir /install WORKDIR /install -ENV PATH=/root/.local/bin:$PATH +# assumes `make dist` as prerequisite COPY ./dist/scanoss-*-py3-none-any.whl /install/ +COPY ./requirements-dev.txt /install/ + +# Install dependencies +RUN pip3 install --no-cache-dir /install/scanoss-*-py3-none-any.whl +RUN pip3 install --no-cache-dir scanoss_winnowing +RUN pip3 install --no-cache-dir -r /install/requirements-dev.txt +RUN pip3 install --no-cache-dir scancode-toolkit-mini +RUN pip3 install --no-cache-dir click==8.2.1 # Temporary workaround for scancode-toolkit-mini (https://github.com/aboutcode-org/scancode-toolkit/issues/4573) + +# Download compile and install typecode-libmagic from source (as there is not ARM wheel available) +ADD https://github.com/nexB/typecode_libmagic_from_sources/archive/refs/tags/v5.39.210212.tar.gz /install/ +RUN tar -xvzf /install/v5.39.210212.tar.gz -C /install \ + && cd /install/typecode_libmagic_from_sources* \ + && ./build.sh \ + && python3 setup.py sdist bdist_wheel \ + && ls /install/typecode_libmagic_from_sources*/dist/*.whl \ + && pip3 install --no-cache-dir `ls /install/typecode_libmagic_from_sources*/dist/*.whl` -#RUN pip3 install --user scanoss -RUN pip3 install --user /install/scanoss-*-py3-none-any.whl -RUN pip3 install --user scancode-toolkit-mini -RUN pip3 install --user typecode-libmagic +RUN pip3 uninstall --no-cache-dir -y -r /install/requirements-dev.txt # Remove license data references as they are not required for dependency scanning (to save space) -RUN rm -rf /root/.local/lib/python3.8/site-packages/licensedcode/data/rules /root/.local/lib/python3.8/site-packages/licensedcode/data/cache -RUN mkdir /root/.local/lib/python3.8/site-packages/licensedcode/data/rules /root/.local/lib/python3.8/site-packages/licensedcode/data/cache +RUN rm -rf /opt/venv/lib/python3.10/site-packages/licensedcode/data/rules /opt/venv/lib/python3.10/site-packages/licensedcode/data/cache +RUN mkdir /opt/venv/lib/python3.10/site-packages/licensedcode/data/rules /opt/venv/lib/python3.10/site-packages/licensedcode/data/cache -FROM base +# Image with no default entry point +FROM base AS no_entry_point # Copy the Python user packages from the build image to here -COPY --from=builder /root/.local /root/.local +COPY --from=builder /opt/venv /opt/venv # Setup the path and explicitly set GRPC Polling strategy -ENV PATH=/root/.local/bin:$PATH +ENV PATH=/opt/venv/bin:$PATH ENV GRPC_POLL_STRATEGY=poll -VOLUME /scanoss +# Install jq and curl commands and ICU runtime library +RUN apt-get update \ + && apt-get install -y --no-install-recommends jq curl libicu72 \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Install syft +RUN curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin + +# Setup working directory and user WORKDIR /scanoss +# Run scancode once to setup any initial files, etc. so that it'll run faster later +RUN scancode --package --only-findings --quiet --json /scanoss/scancode-dependencies.json /scanoss && rm -f /scanoss/scancode-dependencies.json + +# Image with no default entry point +FROM no_entry_point AS jenkins + +# Create scanoss user for compatibility +RUN groupadd -g 1000 jenkins && \ + useradd -u 1000 -g jenkins -m -s /bin/bash jenkins + +# Copy the Python user packages from the build image to here +RUN chown -R jenkins:jenkins /scanoss /opt/venv +USER jenkins + +# Image with a default scanoss-py entry point +FROM no_entry_point AS with_entry_point ENTRYPOINT ["scanoss-py"] CMD ["--help"] diff --git a/Makefile b/Makefile index e05fb34b..95af3519 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,11 @@ #vars +IMAGE_BASE=scanoss-py-base IMAGE_NAME=scanoss-py REPO=scanoss +DOCKER_FULLNAME_BASE=${REPO}/${IMAGE_BASE} DOCKER_FULLNAME=${REPO}/${IMAGE_NAME} +GHCR_FULLNAME_BASE=ghcr.io/${REPO}/${IMAGE_BASE} GHCR_FULLNAME=ghcr.io/${REPO}/${IMAGE_NAME} VERSION=$(shell ./version.py) @@ -12,40 +15,91 @@ VERSION=$(shell ./version.py) .PHONY: help help: ## This help - @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + @awk 'BEGIN {FS = ":.*?## "} /^[0-9a-zA-Z_-]+:.*?## / {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) .DEFAULT_GOAL := help -clean: ## Clean all dev data +clean: date_time_clean ## Clean all dev data @echo "Removing dev and distribution data..." @rm -rf dist/* build/* venv/bin/scanoss-py src/scanoss.egg-info -dev_setup: ## Setup Python dev env for the current user +date_time_clean: ## Setup blank datetime data field + @rm -rf src/scanoss/data/build_date.txt + @echo "" > src/scanoss/data/build_date.txt + +date_time: ## Setup package build date + @rm -rf src/scanoss/data/build_date.txt + python3 date_time.py + +dev_setup: date_time_clean ## Setup Python dev env for the current user @echo "Setting up dev env for the current user..." - python3 setup.py develop --user + pip3 install -e . + +dev_install: ## Install dev dependencies + pip3 install -r requirements-dev.txt dev_uninstall: ## Uninstall Python dev setup for the current user @echo "Uninstalling dev env..." - python3 setup.py develop --user --uninstall + pip3 uninstall -y scanoss @rm -f venv/bin/scanoss-py @rm -rf src/scanoss.egg-info -dist: clean dev_uninstall ## Prepare Python package into a distribution +dist: clean dev_uninstall date_time ## Prepare Python package into a distribution @echo "Build deployable package for distribution $(VERSION)..." - python3 setup.py sdist bdist_wheel + python3 -m build twine check dist/* publish_test: ## Publish the Python package to TestPyPI @echo "Publishing package to TestPyPI..." twine upload --repository testpypi dist/* +unit_test: ## Run unit tests + @echo "Running unit tests..." + @python -m unittest + +lint-docker: ## Run ruff linter with docker + @./tools/linter.sh --docker + +lint-docker-fix: ## Run ruff linter with docker and auto-fix + @./tools/linter.sh --docker --fix + +lint: ## Run ruff linter locally + @./tools/linter.sh + +lint-fix: ## Run ruff linter locally with auto-fix + @./tools/linter.sh --fix + +lint-all: ## Run ruff linter locally for all files + @./tools/linter.sh --all + +lint-fix-all: ## Run ruff linter locally with auto-fix for all files + @./tools/linter.sh --fix --all + publish: ## Publish Python package to PyPI @echo "Publishing package to PyPI..." twine upload dist/* -ghcr_build: dist ## Build GitHub container image +package_all: dist publish ## Build & Publish Python package to PyPI + +ghcr_build: dist ## Build GitHub container image with local arch @echo "Building GHCR container image..." - docker build --no-cache -t $(GHCR_FULLNAME) --platform linux/amd64 . + docker build --target with_entry_point -t $(GHCR_FULLNAME) . + +ghcr_build_base: dist ## Build GitHub container base image with local arch (no entrypoint) + @echo "Building GHCR base container image..." + docker build --target no_entry_point -t $(GHCR_FULLNAME_BASE) . + +ghcr_build_jenkins: dist ## Build GitHub container jenkins image with local arch + @echo "Building GHCR base container image..." + docker build --target jenkins -t $(GHCR_FULLNAME_BASE) . + +ghcr_amd64: dist ## Build GitHub AMD64 container image + @echo "Building GHCR AMD64 container image..." + docker build --target with_entry_point -t $(GHCR_FULLNAME) --platform linux/amd64 . + +ghcr_arm64: dist ## Build GitHub ARM64 container image + @echo "Building GHCR ARM64 container image..." + docker build --target with_entry_point -t $(GHCR_FULLNAME) --platform linux/arm64 . ghcr_tag: ## Tag the latest GH container image with the version from Python @echo "Tagging GHCR latest image with $(VERSION)..." @@ -56,11 +110,31 @@ ghcr_push: ## Push the GH container image to GH Packages docker push $(GHCR_FULLNAME):$(VERSION) docker push $(GHCR_FULLNAME):latest -ghcr_all: ghcr_build ghcr_tag ghcr_push ## Execute all GitHub Package container actions +ghcr_release: dist ## Build/Publish GitHub multi-platform container image + @echo "Building & Releasing GHCR multi-platform container image $(VERSION)..." + docker buildx build --push --target with_entry_point -t $(GHCR_FULLNAME):$(VERSION) --platform linux/arm64,linux/amd64 . + +ghcr_all: ghcr_release ## Execute all GHCR container actions + +docker_build: dist ## Build Docker container image with local arch + @echo "Building Docker image..." + docker build --no-cache --target with_entry_point -t $(DOCKER_FULLNAME) . -docker_build: ## Build Docker container image +docker_build_base: dist ## Build Base Docker container image with local arch - no entrypoint @echo "Building Docker image..." - docker build --no-cache -t $(DOCKER_FULLNAME) . + docker build --no-cache --target no_entry_point -t $(DOCKER_FULLNAME_BASE) . + +docker_build_jenkins: dist ## Build Jenkins Docker container image with local arch + @echo "Building Docker image..." + docker build --no-cache --target jenkins -t $(DOCKER_FULLNAME_BASE) . + +docker_amd64: dist ## Build Docker AMD64 container image + @echo "Building Docker AMD64 container image..." + docker build --target with_entry_point -t $(DOCKER_FULLNAME) --platform linux/amd64 . + +docker_arm64: dist ## Build Docker ARM64 container image + @echo "Building Docker ARM64 container image..." + docker build --target with_entry_point -t $(DOCKER_FULLNAME) --platform linux/arm64 . docker_tag: ## Tag the latest Docker container image with the version from Python @echo "Tagging Docker latest image with $(VERSION)..." @@ -71,4 +145,8 @@ docker_push: ## Push the Docker container image to DockerHub docker push $(DOCKER_FULLNAME):$(VERSION) docker push $(DOCKER_FULLNAME):latest -docker_all: docker_build docker_tag docker_push ## Execute all DockerHub container actions +docker_release: dist ## Build/Publish Docker multi-platform container image + @echo "Building & Releasing Docker multi-platform container image $(VERSION)..." + docker buildx build --push --target with_entry_point -t $(DOCKER_FULLNAME):$(VERSION) --platform linux/arm64,linux/amd64 . + +docker_all: docker_release ## Execute all DockerHub container actions diff --git a/PACKAGE.md b/PACKAGE.md index 424a4e02..b2e55309 100644 --- a/PACKAGE.md +++ b/PACKAGE.md @@ -11,10 +11,35 @@ To upgrade an existing installation please run: pip3 install --upgrade scanoss ``` +### Fast Winnowing +To take advantage of faster fingerprinting, please install the optional [scanoss_winnowing](https://pypi.org/project/scanoss_winnowing/) package: +```bash +pip3 install scanoss_winnowing +``` +Or directly using: +```bash +pip3 install scanoss[fast_winnowing] +``` + ### Docker Alternatively, there is a docker image of the compiled package. It can be found [here](https://github.com/scanoss/scanoss.py/pkgs/container/scanoss-py). Details of how to run it can be found [here](https://github.com/scanoss/scanoss.py/blob/main/GHCR.md). +### Externally Managed Environments on Linux +If installing on Ubuntu 2023.04, Fedora 38, Debian 11, etc. a few additional steps are required before installing `scanoss-py`. More details can be found [here](https://itsfoss.com/externally-managed-environment/). + +The recommended method is to install `pipx` and use it to install `scanoss-py`: +```bash +sudo apt install pipx +pipx ensurepath +``` + +This will install the `pipx` package manager, which can then be used to install `scanoss-py`: +```bash +pipx install scanoss[fast_winnowing] +``` +This will install the `scanoss-py` app in a separate virtual environment and create a link to the local path for execution. + ## Usage The package can be run from the command line, or consumed from another Python script. @@ -32,24 +57,32 @@ Running the bare command will list the available sub-commands: ```bash > scanoss-py -usage: scanoss-py [-h] {version,ver,scan,sc,fingerprint,fp,wfp,dependencies,dp,dep} ... +usage: scanoss-py [-h] [--version] + {version,ver,scan,sc,fingerprint,fp,wfp,dependencies,dp,dep,file_count,fc,convert,cv,cnv,cvrt,component,comp,utils,ut} + ... -SCANOSS Python CLI. Ver: 0.9.0, License: MIT +SCANOSS Python CLI. Ver: 1.6.1, License: MIT, Fast Winnowing: True -optional arguments: +options: -h, --help show this help message and exit + --version, -v Display version details Sub Commands: valid subcommands - {version,ver,scan,sc,fingerprint,fp,wfp,dependencies,dp,dep} + {version,ver,scan,sc,fingerprint,fp,wfp,dependencies,dp,dep,file_count,fc,convert,cv,cnv,cvrt,component,comp,utils,ut} sub-command help version (ver) SCANOSS version scan (sc) Scan source code fingerprint (fp, wfp) Fingerprint source code dependencies (dp, dep) - Scan source code for dependencies + Scan source code for dependencies, but do not decorate them + file_count (fc) Search the source tree and produce a file type summary + convert (cv, cnv, cvrt) + Convert file format + component (comp) Component support commands + utils (ut) General utility support commands ``` From there it is possible to scan a source code folder: @@ -60,7 +93,7 @@ From there it is possible to scan a source code folder: #### Scanning for Dependencies The SCANOSS CLI supports dependency decoration. In order for this to work, it requires the installation of scancode: -```python +```bash pip install scancode-toolkit ``` Dependencies can then be decorated by adding the ``--dependencies`` option to the scanner: @@ -84,8 +117,8 @@ if __name__ == "__main__": ``` ## Scanning URL and API Key -By Default, scanoss uses the API URL endpoint for SCANOSS OSS KB: https://osskb.org/api/scan/direct. -This API does not require an API key. +By Default, scanoss uses the API base URL for SCANOSS OSS KB: https://api.osskb.org. +The `/scan/direct` endpoint is automatically appended. This API does not require an API key. These values can be changed from the command line using: ```bash @@ -105,10 +138,13 @@ if __name__ == "__main__": ``` ## Requirements -Python 3.7 or higher. +Python 3.9 or higher. ## Source code The source for this package can be found [here](https://github.com/scanoss/scanoss.py). +## Documentation +For client usage help please look [here](https://github.com/scanoss/scanoss.py/blob/main/CLIENT_HELP.md). + ## Changelog Details of each release can be found [here](https://github.com/scanoss/scanoss.py/blob/main/CHANGELOG.md). diff --git a/README.md b/README.md index 1c4a9e9d..ececd218 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # SCANOSS Python Library The SCANOSS python package provides a simple, easy to consume library for interacting with SCANOSS APIs/Engine. +[![Build/Test Local Package](https://github.com/scanoss/scanoss.py/actions/workflows/python-local-test.yml/badge.svg)](https://github.com/scanoss/scanoss.py/actions/workflows/python-local-test.yml) +[![Build/Test Local Container](https://github.com/scanoss/scanoss.py/actions/workflows/container-local-test.yml/badge.svg)](https://github.com/scanoss/scanoss.py/actions/workflows/container-local-test.yml) +[![Publish Package - PyPI](https://github.com/scanoss/scanoss.py/actions/workflows/python-publish-pypi.yml/badge.svg)](https://github.com/scanoss/scanoss.py/actions/workflows/python-publish-pypi.yml) +[![Publish GHCR Container](https://github.com/scanoss/scanoss.py/actions/workflows/container-publish-ghcr.yml/badge.svg)](https://github.com/scanoss/scanoss.py/actions/workflows/container-publish-ghcr.yml) + # Installation To install (from [pypi.org](https://pypi.org/project/scanoss)), please run: ```bash @@ -19,7 +24,7 @@ To leverage the CLI from within a container, please look at [GHCR.md](GHCR.md). Before starting with development of this project, please read our [CONTRIBUTING](CONTRIBUTING.md) and [CODE OF CONDUCT](CODE_OF_CONDUCT.md). ### Requirements -Python 3.7 or higher. +Python 3.9 or higher. The dependencies can be found in the [requirements.txt](requirements.txt) and [requirements-dev.txt](requirements-dev.txt) files. @@ -29,12 +34,36 @@ pip3 install -r requirements.txt pip3 install -r requirements-dev.txt ``` -To enable dependency scanning an extra tool is required: scancode-toolkit +To enable dependency scanning, an extra tool is required: scancode-toolkit +```bash +pip3 install -r requirements-scancode.txt +``` + +### Pre-commit Setup +This project uses pre-commit hooks to ensure code quality and consistency. To set up pre-commit, run: ```bash -pip3 install typecode-libmagic -pip3 install scancode-toolkit-mini +pip3 install pre-commit +pre-commit install ``` +This will install the pre-commit tool and set up the git hooks defined in the `.pre-commit-config.yaml` file to run automatically on each commit. + +### Devcontainer Setup +To simplify the development environment setup, a devcontainer configuration is provided. This allows you to develop inside a containerized environment with all necessary dependencies pre-installed. + +To use the devcontainer setup: +1. Install [Visual Studio Code](https://code.visualstudio.com/). +2. Install the [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension. +3. Open the project in Visual Studio Code. +4. Run +```bash +cp .devcontainer/devcontainer.example.json .devcontainer/devcontainer.json +``` +5. Update the `devcontainer.json` file with the desired settings. +6. When prompted, reopen the project in the container. + +This will build the container defined in the `.devcontainer` folder and open a new Visual Studio Code window connected to the container. + ### Package Development More details on Python packaging/distribution can be found [here](https://packaging.python.org/overview/), [here](https://packaging.python.org/guides/distributing-packages-using-setuptools/), and [here](https://packaging.python.org/guides/using-testpypi/#using-test-pypi). @@ -85,6 +114,19 @@ The package will then be available to install using: pip3 install scanoss ``` +##### GitHub Actions +There are a number of [workflows](.github/workflows) setup for this repository. They provide the following: +* [Local build/test](.github/workflows/python-local-test.yml) + * Automatically triggered on pushes or PRs to main. Can also be run manually for other branches +* [Local container build/test](.github/workflows/container-local-test.yml) + * Automatically triggered on pushes or PRs to main. Can also be run manually for other branches +* [Publish to Test PyPI](.github/workflows/python-publish-testpypi.yml) + * Can be manually triggered to push a test version from any branch +* [Publish to PyPI](.github/workflows/python-publish-pypi.yml) + * Build and publish the Python package to PyPI (triggered by v*.*.* tag) +* [Publish container to GHCR](.github/workflows/container-publish-ghcr.yml) + * Build and publish the Python container to GHCR (triggered by v*.*.* tag) + ## Bugs/Features To request features or alert about bugs, please do so [here](https://github.com/scanoss/scanoss.py/issues). @@ -93,3 +135,8 @@ Details of major changes to the library can be found in [CHANGELOG.md](CHANGELOG ## Background Details about the Winnowing algorithm used for scanning can be found [here](WINNOWING.md). + +## Dataset License Notice +This application is licensed under the MIT License. In addition, it includes an unmodified copy of the OSADL copyleft license dataset ([osadl-copyleft.json](src/scanoss/data/osadl-copyleft.json)) which is licensed under the [Creative Commons Attribution 4.0 International license (CC-BY-4.0)](https://creativecommons.org/licenses/by/4.0/) by the [Open Source Automation Development Lab (OSADL) eG](https://www.osadl.org/). + +**Attribution:** A project by the Open Source Automation Development Lab (OSADL) eG. Original source: [https://www.osadl.org/fileadmin/checklists/copyleft.json](https://www.osadl.org/fileadmin/checklists/copyleft.json) diff --git a/SBOM.json b/SBOM.json index 7f6d85d0..c21c88d9 100644 --- a/SBOM.json +++ b/SBOM.json @@ -1,45 +1,159 @@ -[ - { - "vendor": "scanoss", - "component": "scanoss.py", - "purl": "pkg:github/scanoss/scanoss.py", - "dependency": "self", - "comment": "scanoss.py is the implementation, everything else is dependencies", - "license": "MIT", - "license_url": "https://raw.githubusercontent.com/scanoss/scanoss.py/main/LICENSE" - }, - { - "vendor": "ICRAR", - "component": "crc32c", - "dependency": "runtime", - "homepage": "https://github.com/ICRAR/crc32c", - "purl": "pkg:github/ICRAR/crc32c", - "license_url": "https://raw.githubusercontent.com/ICRAR/crc32c/master/LICENSE", - "license": "LGPL-2.1-only" - }, - { - "vendor": "pst", - "component": "requests", - "dependency": "runtime", - "homepage": "https://github.com/psf/requests", - "purl": "pkg:github/psf/requests", - "license_url": "https://raw.githubusercontent.com/psf/requests/master/LICENSE", - "license": "Apache-2.0" - }, - { - "vendor": "audreyfeldroy", - "component": "binaryornot", - "dependency": "runtime", - "homepage": "https://github.com/audreyfeldroy/binaryornot", - "license": "BSD-3-Clause" - }, - { - "vendor": "verigak", - "component": "progress", - "dependency": "runtime", - "homepage": "https://github.com/verigak/progress", - "purl": "pkg:github/verigak/progress", - "license_url": "https://opensource.org/licenses/ISC", - "license": "ISC" - } -] +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:12fa7d1c-20bd-4b2c-8ed3-b2609cb6f7fb", + "version": 1, + "components": [ + { + "type": "library", + "name": "requests", + "publisher": "psf", + "version": "2.28.2", + "purl": "pkg:pypi/requests", + "bom-ref": "pkg:pypi/requests", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ] + }, + { + "type": "library", + "name": "crc32c", + "publisher": "ICRAR", + "version": "2.3", + "purl": "pkg:pypi/crc32c", + "bom-ref": "pkg:pypi/crc32c", + "licenses": [ + { + "license": { + "name": "LGPL-2.1-only" + } + } + ] + }, + { + "type": "library", + "name": "binaryornot", + "publisher": "audreyfeldroy", + "version": "0.4.4", + "purl": "pkg:pypi/binaryornot", + "bom-ref": "pkg:pypi/binaryornot", + "licenses": [ + { + "license": { + "id": "BSD-1-Clause" + } + } + ] + }, + { + "type": "library", + "name": "progress", + "publisher": "verigak", + "version": "1.6", + "purl": "pkg:pypi/progress", + "bom-ref": "pkg:pypi/progress", + "licenses": [ + { + "license": { + "id": "ISC" + } + } + ] + }, + { + "type": "library", + "name": "grpcio", + "publisher": "google", + "version": "1.51.3", + "purl": "pkg:pypi/grpcio", + "bom-ref": "pkg:pypi/grpcio", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ] + }, + { + "type": "library", + "name": "protobuf", + "publisher": "google", + "version": "4.22.1", + "purl": "pkg:pypi/protobuf", + "bom-ref": "pkg:pypi/protobuf", + "licenses": [ + { + "license": { + "name": "BSD-3-Clause" + } + } + ] + }, + { + "type": "library", + "name": "PyPAC", + "publisher": "carsonyl", + "version": "0.16.1", + "purl": "pkg:pypi/pypac", + "bom-ref": "pkg:pypi/pypac", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ] + }, + { + "type": "library", + "name": "pyopenssl", + "publisher": "pyca", + "version": "23.0.0", + "purl": "pkg:pypi/pyopenssl", + "bom-ref": "pkg:pypi/pyopenssl", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ] + }, + { + "type": "library", + "name": "scanoss-winnowing", + "publisher": "scanoss", + "version": "0.1.0", + "purl": "pkg:pypi/scanoss-winnowing", + "bom-ref": "pkg:pypi/scanoss-winnowing", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ] + }, + { + "type": "library", + "name": "scanoss.py", + "publisher": "scanoss", + "version": "0.9.0", + "purl": "pkg:github/scanoss/scanoss.py", + "bom-ref": "pkg:github/scanoss/scanoss.py", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ] + } + ], + "vulnerabilities": [] +} diff --git a/cert_download.sh b/cert_download.sh new file mode 100755 index 00000000..82b3bc83 --- /dev/null +++ b/cert_download.sh @@ -0,0 +1,103 @@ +#!/bin/bash +### +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2022, SCANOSS +# +# 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. +### +# +# Attempt to download an SSL certificate from the specified host and convert to a PEM file +# + +script_name=$(basename "$0") + +help() +{ + echo "Usage: $script_name -n [-p ] [-o pem-file] [-f] [-h] + -n -- Hostname to download certificate from + -p -- Port number to use (default 443) + -o -- Output filename (default .pem) + -f -- Force the overwrite of existing pem file" + exit 2 +} + +SHORT=n:,p:o:,h,f +OPTS=$(getopt $SHORT "$@") +if [[ $? -ne 0 ]]; then + help +fi +VALID_ARGUMENTS=$# +if [ "$VALID_ARGUMENTS" -eq 0 ]; then # No arguments supplied, print help + help +fi +set -- "$OPTS" + +force=0 +while :; do +# echo "1: $1 - 2: $2" + case "$1" in + -n ) + host="$2" + shift 2 + ;; + -p ) + port="$2" + shift 2 + ;; + -o ) + pemfile="$2" + shift 2 + ;; + -f ) + force=1 + shift + ;; + -h ) + help + ;; + --) + shift; + break + ;; + *) + echo "Unexpected option: $1" + help + ;; + esac +done + +if [ -z "$host" ] ; then + echo "Error: Please provide a hostname -h " + exit 1 +fi +if [ -z "$port" ] ; then + port="443" +fi +if [ -z "$pemfile" ] ; then + pemfile="${host}.pem" +fi + +if [ $force -eq 0 ] && [ -f "$pemfile" ] ; then + echo "Error: Output PEM file already exists: $pemfile" + exit 1 +fi +echo "Attempting to get PEM certificate from $host:$port and saving to $pemfile ..." + +openssl s_client -showcerts -verify 5 -connect "$host:$port" -servername "$host" < /dev/null 2> /dev/null | awk '/BEGIN/,/END/{ if(/BEGIN/){a++}; print}' > "$pemfile" diff --git a/date_time.py b/date_time.py new file mode 100755 index 00000000..791d3d34 --- /dev/null +++ b/date_time.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2022, SCANOSS + + 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. +""" + +import datetime + +""" +Store the current date/time into a data field for processing +""" +if __name__ == '__main__': + now = datetime.datetime.now() + data = f'date: {now.strftime("%Y%m%d%H%M%S")}, utime: {int(now.timestamp())}' + with open('src/scanoss/data/build_date.txt', 'w') as f: + f.write(f'{data}\n') diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d0c3cbf1 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..747ffb7b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt new file mode 100644 index 00000000..52b04f2e --- /dev/null +++ b/docs/requirements-docs.txt @@ -0,0 +1 @@ +sphinx_rtd_theme \ No newline at end of file diff --git a/docs/source/_static/scanoss-settings-schema.json b/docs/source/_static/scanoss-settings-schema.json new file mode 100644 index 00000000..153ba9d2 --- /dev/null +++ b/docs/source/_static/scanoss-settings-schema.json @@ -0,0 +1,352 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$comment": "DEPRECATED: The canonical schema is now maintained at https://github.com/scanoss/schema/blob/main/scanoss-settings-schema.json", + "title": "Scanoss Settings", + "type": "object", + "properties": { + "self": { + "type": "object", + "description": "Description of the project under analysis", + "properties": { + "name": { + "type": "string", + "description": "Name of the project" + }, + "license": { + "type": "string", + "description": "License of the project" + }, + "description": { + "type": "string", + "description": "Description of the project" + } + } + }, + "settings": { + "type": "object", + "description": "Scan settings and other configurations", + "properties": { + "skip": { + "type": "object", + "description": "Set of rules to skip files from fingerprinting and scanning", + "properties": { + "patterns": { + "type": "object", + "properties": { + "scanning": { + "type": "array", + "description": "List of glob patterns to skip files from scanning", + "items": { + "type": "string", + "examples": [ + "path/to/folder", + "path/to/folder/**", + "path/to/folder/**/*", + "path/to/file.c", + "path/to/another/file.py", + "**/*.ts", + "**/*.json" + ] + }, + "uniqueItems": true + }, + "fingerprinting": { + "type": "array", + "description": "List of glob patterns to skip files from fingerprinting", + "items": { + "type": "string", + "examples": [ + "path/to/folder", + "path/to/folder/**", + "path/to/folder/**/*", + "path/to/file.c", + "path/to/another/file.py", + "**/*.ts", + "**/*.json" + ] + }, + "uniqueItems": true + } + } + }, + "sizes": { + "type": "object", + "description": "Set of rules to skip files based on their size.", + "properties": { + "scanning": { + "type": "array", + "items": { + "type": "object", + "properties": { + "patterns": { + "type": "array", + "description": "List of glob patterns to apply the min/max size rule", + "items": { + "type": "string", + "examples": [ + "path/to/folder", + "path/to/folder/**", + "path/to/folder/**/*", + "path/to/file.c", + "path/to/another/file.py", + "**/*.ts", + "**/*.json" + ] + } + }, + "min": { + "type": "integer", + "description": "Minimum size of the file in bytes" + }, + "max": { + "type": "integer", + "description": "Maximum size of the file in bytes" + } + } + } + }, + "fingerprinting": { + "type": "array", + "items": { + "type": "object", + "properties": { + "patterns": { + "type": "array", + "description": "List of glob patterns to apply the min/max size rule", + "items": { + "type": "string" + }, + "examples": [ + "path/to/folder", + "path/to/folder/**", + "path/to/folder/**/*", + "path/to/file.c", + "path/to/another/file.py", + "**/*.ts", + "**/*.json" + ] + }, + "min": { + "type": "integer", + "description": "Minimum size of the file in bytes" + }, + "max": { + "type": "integer", + "description": "Maximum size of the file in bytes" + } + } + } + } + } + } + } + }, + "proxy": { + "type": "object", + "description": "Proxy configuration for API requests", + "properties": { + "host": { + "type": "string", + "description": "Proxy host URL" + } + } + }, + "http_config": { + "type": "object", + "description": "HTTP configuration for API requests", + "properties": { + "base_uri": { + "type": "string", + "description": "Base URI for API requests" + }, + "ignore_cert_errors": { + "type": "boolean", + "description": "Whether to ignore certificate errors" + } + } + }, + "file_snippet": { + "type": "object", + "description": "File snippet scanning configuration", + "properties": { + "proxy": { + "type": "object", + "description": "Proxy configuration for file snippet requests", + "properties": { + "host": { + "type": "string", + "description": "Proxy host URL" + } + } + }, + "http_config": { + "type": "object", + "description": "HTTP configuration for file snippet requests", + "properties": { + "base_uri": { + "type": "string", + "description": "Base URI for file snippet API requests" + }, + "ignore_cert_errors": { + "type": "boolean", + "description": "Whether to ignore certificate errors" + } + } + }, + "ranking_enabled": { + "type": ["boolean", "null"], + "description": "Enable/disable ranking", + "default": null + }, + "ranking_threshold": { + "type": ["integer", "null"], + "description": "Ranking threshold value. A value of -1 defers to server configuration", + "minimum": -1, + "maximum": 10, + "default": -1 + }, + "min_snippet_hits": { + "type": "integer", + "description": "Minimum snippet hits required", + "minimum": 0, + "default": 0 + }, + "min_snippet_lines": { + "type": "integer", + "description": "Minimum snippet lines required", + "minimum": 0, + "default": 0 + }, + "honour_file_exts": { + "type": ["boolean", "null"], + "description": "Ignores file extensions. When not set, defers to server configuration.", + "default": true + }, + "dependency_analysis": { + "type": "boolean", + "description": "Enable dependency analysis" + }, + "skip_headers": { + "type": "boolean", + "description": "Skip license headers, comments and imports at the beginning of files", + "default": false + }, + "skip_headers_limit": { + "type": "integer", + "description": "Maximum number of lines to skip when filtering headers", + "default": 0 + } + } + } + } + }, + "bom": { + "type": "object", + "description": "BOM Rules: Set of rules that will be used to modify the BOM before and after the scan is completed", + "properties": { + "include": { + "type": "array", + "description": "Set of rules to be added as context when scanning. This list will be sent as payload to the API.", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "File path", + "examples": ["/path/to/file", "/path/to/another/file"], + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "purl": { + "type": "string", + "description": "Package URL to be used to match the component", + "examples": [ + "pkg:npm/vue@2.6.12", + "pkg:golang/github.com/golang/go@1.17.3" + ] + }, + "comment": { + "type": "string", + "description": "Additional notes or comments" + } + }, + "uniqueItems": true, + "required": ["purl"] + } + }, + "remove": { + "type": "array", + "description": "Set of rules that will remove files from the results file after the scan is completed.", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "File path", + "examples": ["/path/to/file", "/path/to/another/file"] + }, + "purl": { + "type": "string", + "description": "Package URL", + "examples": [ + "pkg:npm/vue@2.6.12", + "pkg:golang/github.com/golang/go@1.17.3" + ] + }, + "comment": { + "type": "string", + "description": "Additional notes or comments" + } + }, + "uniqueItems": true, + "required": ["purl"] + } + }, + "replace": { + "type": "array", + "description": "Set of rules that will replace components with the specified one after the scan is completed.", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "File path", + "examples": ["/path/to/file", "/path/to/another/file"] + }, + "purl": { + "type": "string", + "description": "Package URL to replace", + "examples": [ + "pkg:npm/vue@2.6.12", + "pkg:golang/github.com/golang/go@1.17.3" + ] + }, + "comment": { + "type": "string", + "description": "Additional notes or comments" + }, + "license": { + "type": "string", + "description": "License of the component. Should be a valid SPDX license expression", + "examples": ["MIT", "Apache-2.0"] + }, + "replace_with": { + "type": "string", + "description": "Package URL to replace with", + "examples": [ + "pkg:npm/vue@2.6.12", + "pkg:golang/github.com/golang/go@1.17.3" + ] + } + }, + "uniqueItems": true, + "required": ["purl", "replace_with"] + } + } + } + } + } +} + diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..4372835b --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,29 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + + +project = 'scanoss-py' +copyright = '2024, Scan Open Source Solutions SL' +author = 'Scan Open Source Solutions SL' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ['sphinx_rtd_theme'] + +templates_path = ['_templates'] +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + + +html_theme = 'sphinx_rtd_theme' +html_logo = 'scanosslogo.jpg' +html_static_path = ['_static'] diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000..c1515c4d --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,540 @@ +======================================= +Documentation for scanoss-py +======================================= + +Introduction +============ +In order to complete a Software Composition Analysis of your project, you will +need to scan the fingerprints of the source code against a knowledge base (for example, the `Open Source Software Knowledge Base `_). + +Notice we mention fingerprints, and not the source code itself. Keeping the privacy of your information is the most important rule we follow, +and what makes us different than our competitors. In order to achieve this, the SCANOSS Platform calculates file and snippet fingerprints +(32-bit identifiers calculated with the `winnowing algorithm `_). + +The fingerprints of each file or snippet are then sent to the `SCANOSS API `_, that means you are scanning against the knowledge base and not the other +way around. + +One way to query the SCANOSS Platform is through our Python package: `scanoss-py `_. + +.. note:: + All of SCANOSS software is open source and free to use, explore our `GitHub Organization page `_. You can contribute to this tool, for more information check the `contribution guidelines `_ for this project. + +Features +======== +* The package can be run from the command line, or consumed from another Python script +* Scan your source code fingerprints against a knowledge base +* Dependency detection +* Decoration services for cryptographic algorithm, vulnerabilities, semgrep issues/findings and component version detection +* Generate an SBOM (software bill of materials) in SPDX-Lite and CycloneDX + +Installation +============ +To install (from `pypi.org `_), run: ``pip3 install scanoss``. + +------------ +Requirements +------------ + +Python 3.9 or higher. + +The dependencies can be found in the `requirements.txt `_ and `requirements-dev.txt `_ files. + +To install dependencies run: ``pip3 install -r requirements.txt`` and ``pip3 install requirements-dev.txt``. + +To enable dependency scanning, an extra tool is required: scancode-toolkit. + +To install it run: ``pip3 install -r requirements-scancode.txt`` + + +.. include:: scanoss_settings_schema.rst + + +Commands and arguments +====================== + + +------------------ +Scanning: scan, sc +------------------ + +Scans a directory or file (source code or ``.wfp`` fingerprint file) and shows results on the STDOUT, by default. This command is highly customizable, from the output format to the matching selection logic using an SBOM file, everything can be set to your preference. + +.. code-block:: bash + + scanoss-py scan + + +.. list-table:: + :widths: 20 30 + :header-rows: 1 + + * - Argument + - Description + * - --wfp , -w + - Allows to scan a wfp (winnowing fingerprint) file instead of a directory + * - --dep , -p + - Use a dependency file instead of a directory + * - --identify , -i + - Scan and identify components in SBOM file (an API key is required for this feature) + * - --ignore , -n + - Ignore components specified in the IGNORE SBOM file (an API key is required for this feature) + * - --format , -f + - Indicates the result output format: {plain, cyclonedx, spdxlite, csv} (optional - default plain) + * - --flags , -F + - Sends scanning flags (or definitions) + * - --threads , -T + - Number of threads to use while scanning (optional - default 10 - max 30) + * - --skip-snippets, -S + - Skip the generation of snippets + * - --post-size , -P + - Number of kilobytes to limit the post to while scanning (optional - default 64) + * - --timeout , -M + - Timeout (in seconds) for API communication (optional - default 120) + * - --all folders + - Scan all folders + * - --all-extensions + - Scan all file extensions + * - --all-hidden + - Scan all hidden file/folders + * - --obfuscate + - Obfuscate fingerprints + * - --dependencies, -D + - Add dependency scanning + * - --dependencies-only + - Run dependency scanning only + * - --sc-command + - Scancode command and path if required (optional - default scancode) + * - --sc-timeout + - Timeout (in seconds) for Scancode to complete (optional - default 600) + * - --apiurl + - SCANOSS API base URL (optional - default https://api.osskb.org) + * - --ignore-cert-errors + - Ignore certificate errors + * - --key , -k + - SCANOSS API Key token (optional - not required for default API_URL) + * - --proxy + - Proxy URL to use for connections, can also use the environment variable ``HTTPS_PROXY`` (optional) + * - --pac + - Proxy auto configuration (optional). + * - --ca-cert + - Alternative certificate PEM file, can also use the environment variables ``REQUEST_CA_BUNDLE`` and ``GRPC_DEFAULT_SSL_ROOTS_FILE_PATH`` (optional) +------------------------------------------- +Generate fingerprints: fingerprint, fp, wfp +------------------------------------------- + +Calculates hashes for a directory or file and shows them on the STDOUT. + +.. code-block:: bash + + scanoss-py fingerprint + + +.. list-table:: + :widths: 20 30 + :header-rows: 1 + + * - Argument + - Description + * - --output , -o + - Output result file name (optional - default STDOUT) + * - --obfuscate + - Obfuscate fingerprints + * - --skip-snippets, -S + - Skip the generation of snippets + * - --all-extensions + - Fingerprint all file extensions + * - --all-folders + - Fingerprint all folders + * - --all-hidden + - Fingerprint all hidden files/folders + +----------------------------------------- +Detect dependencies: dependencies, dp, dep +----------------------------------------- + +Scan source code for dependencies, but do not decorate them. + +.. code-block:: bash + + scanoss-py dependencies <> + + +.. list-table:: + :widths: 20 30 + :header-rows: 1 + + * - Argument + - Description + * - --output , -o + - Output result file name (optional - default STDOUT) + * - --container + - Analyze dependencies from a Docker container image instead of a directory + * - --sc-command SC_COMMAND + - Scancode command and path if required (optional - default scancode) + * - --sc-timeout SC_TIMEOUT + - Timeout (in seconds) for scancode to complete (optional - default 600) + +.. note:: + Remember that in order to enable dependency scanning, an extra tool is required: **scancode-toolkit**. To install it, run: ``pip3 install -r requirements-scancode.txt``. + +-------------------------- +File count: file_count, fc +-------------------------- + +Search the source tree and produce a file type summary. + +.. code-block:: bash + + scanoss-py file_count + + +.. list-table:: + :widths: 20 30 + :header-rows: 1 + + * - Argument + - Description + * - --output , -o + - Output result file name (optional - default STDOUT) + * - --all-hidden + - Scan all hidden files/directories + +----------------------------------------- +Format conversion: convert, cv, cnv, cvrt +----------------------------------------- + +Convert file format to plain, SPDX-Lite, CycloneDX or csv. + +.. code-block:: bash + + scanoss-py convert -i --format -o + + +.. list-table:: + :widths: 20 30 + :header-rows: 1 + + * - Argument + - Description + * - -input , -i + - Input file name. + * - --output , -o + - Output result file name (optional - default STDOUT) + * - --format , -f + - Indicates the result output format: {plain, cyclonedx, spdxlite, csv}. (optional - default plain) + +-------------------------------- +Folder Scanning: folder-scan, fs +-------------------------------- + +Performs a comprehensive scan of a directory using folder hashing to identify components and their matches. + +.. code-block:: bash + + scanoss-py folder-scan + +.. list-table:: + :widths: 20 30 + :header-rows: 1 + + * - Argument + - Description + * - --output , -o + - Output result file name (optional - default STDOUT) + * - --format , -f + - Output format: {json, cyclonedx} (optional - default json) + * - --timeout , -M + - Timeout in seconds for API communication (optional - default 600) + * - --rank-threshold + - Filter results to only show those with rank value at or below this threshold (e.g., --rank-threshold 3 returns results with rank 1, 2, or 3). Lower rank values indicate higher quality matches. + * - --settings , -st + - Settings file to use for scanning (optional - default scanoss.json) + * - --skip-settings-file, -stf + - Skip default settings file (scanoss.json) if it exists + * - --key , -k + - SCANOSS API Key token (optional - not required for default OSSKB URL) + * - --proxy + - Proxy URL to use for connections + * - --pac + - Proxy auto configuration. Specify a file, http url or "auto" + * - --ca-cert + - Alternative certificate PEM file + +-------------------------------- +Folder Hashing: folder-hash, fh +-------------------------------- + +Generates cryptographic hashes for files in a given directory and its subdirectories. + +.. code-block:: bash + + scanoss-py folder-hash + +.. list-table:: + :widths: 20 30 + :header-rows: 1 + + * - Argument + - Description + * - --output , -o + - Output result file name (optional - default STDOUT) + * - --format , -f + - Output format: {json} (optional - default json) + * - --settings , -st + - Settings file to use for scanning (optional - default scanoss.json) + * - --skip-settings-file, -stf + - Skip default settings file (scanoss.json) if it exists + +Both commands also support these general options: + * --debug, -d: Enable debug messages + * --trace, -t: Enable trace messages + * --quiet, -q: Enable quiet mode + +------------------------------------ +Container Scanning: container-scan, cs +------------------------------------ + +Scans Docker container images for dependencies, extracting and analyzing components within containerized applications. + +.. code-block:: bash + + scanoss-py container-scan -i + +.. list-table:: + :widths: 20 30 + :header-rows: 1 + + * - Argument + - Description + * - --image , -i + - Docker image name and tag to scan (required) + * - --output , -o + - Output result file name (optional - default STDOUT) + * - --include-base-image + - Include base image dependencies in the scan results + * - --format , -f + - Output format: {json} (optional - default json) + * - --timeout , -M + - Timeout in seconds for API communication (optional - default 600) + * - --key , -k + - SCANOSS API Key token (optional - not required for default OSSKB URL) + * - --proxy + - Proxy URL to use for connections + * - --ca-cert + - Alternative certificate PEM file + +----------------- +Crypto: crypto, cr +----------------- + +Provides subcommands to retrieve cryptographic information for components. + +.. code-block:: bash + + scanoss-py crypto + +Subcommands: +~~~~~~~~~~~~ + +**algorithms (alg)** + Retrieve cryptographic algorithms for the given components. + + .. code-block:: bash + + scanoss-py crypto algorithms --purl + + .. list-table:: + :widths: 20 30 + :header-rows: 1 + + * - Argument + - Description + * - --with-range + - Returns the list of versions in the specified range that contains cryptographic algorithms. (Replaces the previous --range option) + +**hints** + Retrieve encryption hints for the given components. + + .. code-block:: bash + + scanoss-py crypto hints --purl + + .. list-table:: + :widths: 20 30 + :header-rows: 1 + + * - Argument + - Description + * - --with-range + - Returns the list of versions in the specified range that contains encryption hints. + +**versions-in-range (vr)** + Given a list of PURLs and version ranges, get a list of versions that do/don't contain crypto algorithms. + + .. code-block:: bash + + scanoss-py crypto versions-in-range --purl + +Common Crypto Arguments: +~~~~~~~~~~~~~~~~~~~~~~~~ +The following arguments are common to the ``algorithms``, ``hints``, and ``versions-in-range`` subcommands: + +.. list-table:: + :widths: 20 30 + :header-rows: 1 + + * - Argument + - Description + * - --purl , -p + - Package URL (PURL) to process. Can be specified multiple times. + * - --input , -i + - Input file name containing PURLs. + * - --output , -o + - Output result file name (optional - default STDOUT). + * - --timeout , -M + - Timeout (in seconds) for API communication (optional - default 600). + * - --key , -k + - SCANOSS API Key token (optional - not required for default OSSKB URL). + * - --ca-cert + - Alternative certificate PEM file. + * - --debug, -d + - Enable debug messages. + * - --trace, -t + - Enable trace messages, including API posts. + * - --quiet, -q + - Enable quiet mode. + +----------------- +Component: +----------------- + +To be done + +------------------------ +Utilities: utilities, ut +------------------------ + +.. code-block:: bash + + scanoss-py utilities + + +.. list-table:: + :widths: 20 30 + :header-rows: 1 + + * - Argument + - Description + * - fast + - SCANOSS fast winnowing (requires the `SCANOSS Winnowing Python Package `_) + * - certloc, cl + - Display the location of Python CA certificates + * - cert-download, cdl, cert-dl + - Download the specified server's SSL PEM certificate + * - pac-proxy, pac + - Use Proxy Auto-Config to determine proxy configuration + +----------------- +General Arguments +----------------- + +.. list-table:: + :widths: 20 30 + :header-rows: 1 + + * - Argument + - Description + * - -debug, -d + - Enable debug messages + * - --trace, -t + - Enable trace messages, including API posts + * - --quiet, -q + - Enable quiet mode + + +Package consumption +==================== + +This package can be run from the command line, or consumed from another Python script. A good example of how to consume it can be found in this `file `_. + + +In general the easiest way is to import the required module as follows: + +.. code-block:: python + + from scanoss.scanner import Scanner + + def main(): + scanner = Scanner() + scanner.scan_folder( '.' ) + + if __name__ == "__main__": + main() + + +Alternatively, there is a docker image of the compiled package, which can be found in this `repository `_. Details on how to run it can be found in this `file `_. + + +Integrations +============ + +At SCANOSS we want to provide **easy recipes for practical solutions**, that is the reason we are constantly working on building integrations for our software. No need to adapt your existing systems to work with our software, we will adapt our software to your needs. + + +From CI/CD integrations with `Jenkins `_ and `GitHub Actions `_, to our `SonarQube plugin `_ and our most recent `VSCode extension `_. We are always working on making our software as easy to access, consume and integrate as possible. + + +The full list of existing integrations is down below: + +.. list-table:: + :widths: 20 30 + :header-rows: 1 + + * - Integration + - Description + * - `Jenkins `_ + - Integrate scanoss-py into your pipelines + * - `GitHub Actions `_ + - Enhance your software development process with the SCANOSS Code Scan Action + * - `SonarQube `_ + - Scan your code with the SCANOSS plugin for SonarQube + * - `Visual Studio Code `_ + - Software Composition Analysis as you code + +Best practices +============== + +| + +---------------------------------------------------------------------- +*Choose the tool based on your use case, and not the other way around* +---------------------------------------------------------------------- + +SCANOSS offers many tools and software in the field of Software Composition Analysis, and many have similar features. + + +For example, you can perform scans and generate software bill of materials (SBOM) with scanoss-py and the `SBOM Workbench `_, but that doesn't mean these tools are interchangeable. The SBOM Workbench's GUI can be an advantage for auditors and such, but may be a complication for developers that need to integrate an SCA solution into an existing workflow. + + +There is also the case for language preferences, we also offer a `Javascript package `_ and a `Java SDK `_ so you have freedom to consume the SCANOSS API however you want. + +| + +------------------------------- +*Get the most accurate results* +------------------------------- + + +License +======= +The Scanoss Open Source scanoss-py package is released under the MIT license. + + +.. toctree:: + :maxdepth: 2 + :hidden: + :caption: Links + + SCANOSS Website + GitHub + Software transparency foundation diff --git a/docs/source/scanoss_settings_schema.rst b/docs/source/scanoss_settings_schema.rst new file mode 100644 index 00000000..14afa9d9 --- /dev/null +++ b/docs/source/scanoss_settings_schema.rst @@ -0,0 +1,535 @@ +Settings File +====================== + +.. warning:: **Deprecated** — This documentation is no longer maintained here. + The settings schema and its documentation have moved to the + `scanoss/schema `_ repository. + Please refer to the `interactive docs `_ + or the `canonical JSON Schema `_ + for the latest version. + +SCANOSS provides a settings file to customize the scanning process. The settings file is a JSON file that contains project information and BOM (Bill of Materials) rules. It allows you to include, remove, or replace components in the BOM before and after scanning. + +The schema is available to download :download:`here ` + +Schema Overview +--------------- + +The settings file consists of two main sections: + +Project Information +------------------- + +The ``self`` section contains basic information about your project: + +.. code-block:: json + + { + "self": { + "name": "my-project", + "license": "MIT", + "description": "Project description" + } + } + + +Settings +======== +The ``settings`` object allows you to configure various aspects of the scanning process. Currently, it provides control over which files should be skipped during scanning through the ``skip`` property. + +Skip Configuration +------------------ +The ``skip`` object lets you define rules for excluding files from being scanned or fingerprinted. This can be useful for improving scan performance and avoiding unnecessary processing of certain files. + +Properties +~~~~~~~~~~ + +skip.patterns.scanning +^^^^^^^^^^^^^^^^^^^^^^ +A list of patterns that determine which files should be skipped during scanning. The patterns follow the same format as ``.gitignore`` files. For more information, see the `gitignore patterns documentation `_. + +:Type: Array of strings +:Required: No +:Example: + .. code-block:: json + + { + "settings": { + "skip": { + "patterns": { + "scanning": [ + "*.log", + "!important.log", + "temp/", + "debug[0-9]*.txt", + "src/client/specific-file.js", + "src/nested/folder/" + ] + } + } + } + } + +skip.patterns.fingerprinting +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +A list of patterns that determine which files should be skipped during fingerprinting. The patterns follow the same format as ``.gitignore`` files. For more information, see the `gitignore patterns documentation `_. + +:Type: Array of strings +:Required: No +:Example: + .. code-block:: json + + { + "settings": { + "skip": { + "patterns": { + "fingerprinting": [ + "*.log", + "!important.log", + "temp/", + "debug[0-9]*.txt", + "src/client/specific-file.js", + "src/nested/folder/" + ] + } + } + } + } + +skip.sizes.scanning +^^^^^^^^^^^^^^^^^^^ +Rules for skipping files based on their size during scanning. + +:Type: Object +:Required: No +:Properties: + * ``patterns`` (array of strings): List of glob patterns to apply the min/max size rule + * ``min`` (integer): Minimum file size in bytes + * ``max`` (integer): Maximum file size in bytes (Required) +:Example: + .. code-block:: json + + { + "settings": { + "skip": { + "sizes": { + "scanning": [ + { + "patterns": [ + "*.log", + "!important.log", + "temp/", + "debug[0-9]*.txt", + "src/client/specific-file.js", + "src/nested/folder/" + ], + "min": 100, + "max": 1000000 + } + ] + } + } + } + } + +skip.sizes.fingerprinting +^^^^^^^^^^^^^^^^^^^^^^^^^ +Rules for skipping files based on their size during fingerprinting. + +:Type: Object +:Required: No +:Properties: + * ``patterns`` (array of strings): List of glob patterns to apply the min/max size rule + * ``min`` (integer): Minimum file size in bytes + * ``max`` (integer): Maximum file size in bytes (Required) +:Example: + .. code-block:: json + + { + "settings": { + "skip": { + "sizes": { + "fingerprinting": [ + { + "patterns": [ + "*.log", + "!important.log", + "temp/", + "debug[0-9]*.txt", + "src/client/specific-file.js", + "src/nested/folder/" + ], + "min": 100, + "max": 1000000 + } + ] + } + } + } + } + +Pattern Format Rules +'''''''''''''''''''' +* Patterns are matched **relative to the scan root directory** +* A trailing slash indicates a directory (e.g., ``path/`` matches only directories) +* An asterisk ``*`` matches anything except a slash +* Two asterisks ``**`` match zero or more directories (e.g., ``path/**/folder`` matches ``path/to``, ``path/to/folder``, ``path/to/folder/b``) +* Range notations like ``[0-9]`` match any character in the range +* Question mark ``?`` matches any single character except a slash + + +Examples with Explanations +'''''''''''''''''''''''''' +.. code-block:: none + + # Match all .txt files + *.txt + + # Match all .log files except important.log + *.log + !important.log + + # Match all files in the build directory + build/ + + # Match all .pdf files in docs directory and its subdirectories + docs/**/*.pdf + + # Match files like test1.js, test2.js, etc. + test[0-9].js + + + +Scan Tuning Parameters +---------------------- +The SCANOSS scan engine supports tuning parameters for snippet matching. These parameters allow you to fine-tune how the scanner identifies code snippets in your repository. + +.. list-table:: + :header-rows: 1 + :widths: 20 15 10 55 + + * - Parameter + - Type + - Default + - Description + * - ``min_snippet_hits`` + - ``integer`` + - ``0`` + - Minimum snippet hits required. ``0`` defers to server configuration. + * - ``min_snippet_lines`` + - ``integer`` + - ``0`` + - Minimum snippet lines required. ``0`` defers to server configuration. + * - ``ranking_enabled`` + - ``boolean | null`` + - ``null`` + - Enable/disable result ranking. ``null`` defers to server configuration. + * - ``ranking_threshold`` + - ``integer | null`` + - ``0`` + - Ranking threshold value (``-1`` to ``10``). ``-1`` defers to server configuration. + * - ``honour_file_exts`` + - ``boolean | null`` + - ``true`` + - Honour file extensions during matching. ``null`` defers to server configuration. + +Example Configuration +~~~~~~~~~~~~~~~~~~~~~ + +Add the ``file_snippet`` section to your ``scanoss.json`` file: + +.. code-block:: json + + { + "settings": { + "file_snippet": { + "min_snippet_hits": 3, + "min_snippet_lines": 5, + "ranking_enabled": true, + "ranking_threshold": 5, + "honour_file_exts": true + } + } + } + + +Complete Example +------------------- +Here's a comprehensive example combining pattern and size-based skipping: + +.. code-block:: json + + { + "settings": { + "skip": { + "patterns": { + "scanning": [ + "# Node.js dependencies", + "node_modules/", + + "# Build outputs", + "dist/", + "build/" + ], + "fingerprinting": [ + "# Logs except important ones", + "*.log", + "!important.log", + + "# Temporary files", + "temp/", + "*.tmp", + + "# Debug files with numbers", + "debug[0-9]*.txt", + + "# All test files in any directory", + "**/*test.js" + ] + }, + "sizes": { + "scanning": [ + { + "patterns": [ + "*.log", + "!important.log" + ], + "min": 512, + "max": 5242880 + } + ], + "fingerprinting": [ + { + "patterns": [ + "temp/", + "*.tmp", + "debug[0-9]*.txt", + "src/client/specific-file.js", + "src/nested/folder/" + ], + "min": 512, + "max": 5242880 + } + ] + } + } + } + } + +BOM Rules +--------- + +The ``bom`` section defines rules for modifying the BOM before and after scanning. It contains three main operations: + +1. Include Rules +~~~~~~~~~~~~~~~~ + +Rules for adding context when scanning. These rules will be sent to the SCANOSS API meaning they have more chance of being considered part of the resulting scan. + +.. code-block:: json + + { + "bom": { + "include": [ + { + "path": "/path/to/file", + "purl": "pkg:npm/vue@2.6.12", + "comment": "Optional comment" + } + ] + } + } + +2. Remove Rules +~~~~~~~~~~~~~~~ + +Rules for removing files from results after scanning. These rules will be applied to the results file after scanning. The post processing happens on the client side. + +.. code-block:: json + + { + "bom": { + "remove": [ + { + "path": "/path/to/file", + "purl": "pkg:npm/vue@2.6.12", + "comment": "Optional comment" + } + ] + } + } + +3. Replace Rules +~~~~~~~~~~~~~~~~ + +Rules for replacing components after scanning. These rules will be applied to the results file after scanning. The post processing happens on the client side. + +.. code-block:: json + + { + "bom": { + "replace": [ + { + "path": "/path/to/file", + "purl": "pkg:npm/vue@2.6.12", + "replace_with": "pkg:npm/vue@2.6.14", + "license": "MIT", + "comment": "Optional comment" + } + ] + } + } + +Important Notes +--------------- + +Matching Rules +~~~~~~~~~~~~~~ + +1. **Full Match**: Requires both PATH and PURL to match. It means the rule will be applied ONLY to the specific file with the matching PURL and PATH. +2. **Partial Match**: Matches based on either: + - File path only (PURL is optional). It means the rule will be applied to all files with the matching path. + - PURL only (PATH is optional). It means the rule will be applied to all files with the matching PURL. + +File Paths +~~~~~~~~~~ + +- All paths should be specified relative to the scanned directory +- Use forward slashes (``/``) as path separators + +Given the following example directory structure: + +.. code-block:: text + + project/ + ├── src/ + │ └── component.js + └── lib/ + └── utils.py + +- If the scanned directory is ``/project/src``, then: + - ``component.js`` is a valid path + - ``lib/utils.py`` is an invalid path and will not match any files +- If the scanned directory is ``/project``, then: + - ``src/component.js`` is a valid path + - ``lib/utils.py`` is a valid path + +Package URLs (PURLs) +~~~~~~~~~~~~~~~~~~~~ + +PURLs must follow the Package URL specification: + +- Format: ``pkg://@`` +- Examples: + - ``pkg:npm/vue@2.6.12`` + - ``pkg:golang/github.com/golang/go@1.17.3`` +- Must be valid and include all required components +- Version is strongly recommended but optional + +Example Configuration +--------------------- + +Here's a complete example showing all sections: + +.. code-block:: json + + { + "self": { + "name": "example-project", + "license": "Apache-2.0", + "description": "Example project configuration" + }, + "settings": { + "skip": { + "patterns": { + "scanning": [ + "node_modules/", + "dist/", + "build/", + ], + "fingerprinting": [ + "*.log", + "!important.log", + "temp/", + "*.tmp", + "debug[0-9]*.txt", + "**/*test.js" + ] + }, + "sizes": { + "scanning": [ + { + "patterns": [ + "*.log", + "!important.log", + ], + "min": 512, + "max": 5242880 + } + ], + "fingerprinting": [ + { + "patterns": [ + "temp/", + "debug[0-9]*.txt", + "src/client/specific-file.js", + "src/nested/folder/" + ], + "min": 512, + "max": 5242880 + } + ] + } + }, + "file_snippet": { + "min_snippet_hits": 3, + "min_snippet_lines": 5, + "ranking_enabled": true, + "ranking_threshold": 5, + "honour_file_exts": true + } + }, + "bom": { + "include": [ + { + "path": "src/lib/component.js", + "purl": "pkg:npm/lodash@4.17.21", + "comment": "Include lodash dependency" + } + ], + "remove": [ + { + "purl": "pkg:npm/deprecated-pkg@1.0.0", + "comment": "Remove deprecated package" + } + ], + "replace": [ + { + "path": "src/utils/helper.js", + "purl": "pkg:npm/old-lib@1.0.0", + "replace_with": "pkg:npm/new-lib@2.0.0", + "license": "MIT", + "comment": "Upgrade to newer version" + } + ] + } + } + +Usage +----- + +You can pass the settings file path as an argument to the CLI + +.. code-block:: bash + + $ scanoss-py scan . --settings /path/to/settings.json + +If no settings file is provided, the default settings file will be used. +The default location for the settings file is ``scanoss.json`` in the current working directory. +If this file does not exist, settings will be omitted. + +You can also skip the default settings file: + +.. code-block:: bash + + $ scanoss-py scan . --skip-settings-file \ No newline at end of file diff --git a/docs/source/scanosslogo.jpg b/docs/source/scanosslogo.jpg new file mode 100644 index 00000000..7e6708d3 Binary files /dev/null and b/docs/source/scanosslogo.jpg differ diff --git a/examples/sample_code/hello.c b/examples/sample_code/hello.c new file mode 100644 index 00000000..f5023c4e --- /dev/null +++ b/examples/sample_code/hello.c @@ -0,0 +1,9 @@ +/* + * Sample C file for SCANOSS fingerprinting demo + */ +#include + +int main() { + printf("Hello, World!\n"); + return 0; +} diff --git a/examples/sample_code/utils.py b/examples/sample_code/utils.py new file mode 100644 index 00000000..10ebac36 --- /dev/null +++ b/examples/sample_code/utils.py @@ -0,0 +1,25 @@ +""" +Sample Python file for SCANOSS fingerprinting demo +""" + + +def add(a, b): + """Add two numbers.""" + return a + b + + +def subtract(a, b): + """Subtract two numbers.""" + return a - b + + +def multiply(a, b): + """Multiply two numbers.""" + return a * b + + +def divide(a, b): + """Divide two numbers.""" + if b == 0: + raise ValueError("Cannot divide by zero") + return a / b diff --git a/examples/wfp_scan_example.py b/examples/wfp_scan_example.py new file mode 100644 index 00000000..f830d833 --- /dev/null +++ b/examples/wfp_scan_example.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +""" +SCANOSS SDK Example: WFP Generation and Scanning + +This example demonstrates how to: +1. Generate WFP fingerprints from a folder +2. Save fingerprints to disk +3. Reuse saved fingerprints for multiple scans + +Usage: + python wfp_scan_example.py +""" + +import os + +from scanoss.scanner import Scanner +from scanoss.scantype import ScanType + +# Get the directory where this script is located +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) + +# Sample paths relative to this script +SAMPLE_CODE_DIR = os.path.join(SCRIPT_DIR, 'sample_code') +OUTPUT_DIR = os.path.join(SCRIPT_DIR, 'output') +WFP_FILE = os.path.join(OUTPUT_DIR, 'fingerprints.wfp') +RESULTS_FILE = os.path.join(OUTPUT_DIR, 'results.json') + + +def main(): + # Create output directory if it doesn't exist + os.makedirs(OUTPUT_DIR, exist_ok=True) + + # Step 1: Create Scanner instance with options + scanner = Scanner( + debug=True, + quiet=False, + scan_output=RESULTS_FILE, # Where to save scan results + # api_key='your-api-key', # Optional: your SCANOSS API key + scan_options=ScanType.SCAN_FILES.value | ScanType.SCAN_SNIPPETS.value, # File and snippet scanning only + ) + + # Step 2: Generate and save WFP fingerprints to disk + print(f'Generating fingerprints from: {SAMPLE_CODE_DIR}') + print(f'Saving fingerprints to: {WFP_FILE}') + scanner.wfp_folder( + scan_dir=SAMPLE_CODE_DIR, + wfp_file=WFP_FILE, + ) + print('Fingerprints generated successfully!\n') + + # Step 3: Reuse the saved WFP for multiple scans + print(f'Scanning using fingerprints: {WFP_FILE}') + print(f'Results will be saved to: {RESULTS_FILE}') + scanner.scan_wfp_with_options( + wfp_file=WFP_FILE, + deps_file='', # No dependency file needed since we disabled dependency scanning + ) + print('Scan completed!\n') + + #You can run additional scans with the same fingerprints + # scanner.scan_wfp_with_options( + # wfp_file=WFP_FILE, + # deps_file='', # No dependency file needed since we disabled dependency scanning + # ) + + +if __name__ == '__main__': + main() diff --git a/pyproject.toml b/pyproject.toml index 48fb09ac..a3163499 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,27 @@ [build-system] requires = ["setuptools", "wheel", "twine"] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" + +[tool.ruff] +# Enable pycodestyle (E), pyflakes (F), isort (I), pylint (PL) +lint.select = ["E", "F", "I", "PL"] +line-length = 120 +# Assume Python 3.9+ +target-version = "py39" +exclude = [ + "tests/*", + "test_*.py", + "src/protoc_gen_swagger/*", + "src/scanoss/api/*", +] + +[tool.ruff.format] +quote-style = "single" +indent-style = "space" +line-ending = "auto" + +[tool.ruff.lint.isort] +known-first-party = ["scanoss"] + +[tool.ruff.lint.pylint] +max-args = 6 diff --git a/requirements-dev.txt b/requirements-dev.txt index 9c9c1d6c..083ebdf2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,7 @@ setuptools wheel twine +build grpcio-tools +ruff +pre-commit \ No newline at end of file diff --git a/requirements-scancode.txt b/requirements-scancode.txt new file mode 100644 index 00000000..930b9b07 --- /dev/null +++ b/requirements-scancode.txt @@ -0,0 +1,2 @@ +typecode-libmagic +scancode-toolkit-mini diff --git a/requirements.txt b/requirements.txt index 4d5ca4f4..9beb5da0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,16 @@ requests crc32c>=2.2 binaryornot progress -grpcio<=1.42.0 -protobuf<=3.19.1 +grpcio>=1.73.1 +protobuf>=6.3.1 +protoc-gen-openapiv2 +pypac +urllib3 +pyOpenSSL +google-api-core +importlib_resources +packageurl-python +pathspec +jsonschema +crc +cyclonedx-python-lib[validation] \ No newline at end of file diff --git a/scanoss.json b/scanoss.json new file mode 100644 index 00000000..11f807bf --- /dev/null +++ b/scanoss.json @@ -0,0 +1,31 @@ +{ + "settings": { + "skip": { + "patterns": { + "scanning": [ + "src/protoc_gen_swagger", + "docs", + "scanoss_common_pb2_grpc.py", + "tests/data/test_src_files.tar.gz", + "tests/data/src" + ] + }, + "sizes": {} + } + }, + "bom": { + "include": [ + { + "purl": "pkg:github/scanoss/scanoss.py" + }, + { + "purl": "pkg:pypi/scanoss" + }, + { + "purl": "pkg:github/scanoss/scanoss-winnowing.py" + } + ], + "remove": [] + } +} + diff --git a/setup.cfg b/setup.cfg index eed67f95..602b0b3e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,28 +3,56 @@ name = scanoss version = attr: scanoss.__version__ author = SCANOSS author_email = info@scanoss.com -description = Simple Python library to use the SCANOSS APIs +license = MIT +description = Simple Python library to leverage the SCANOSS APIs long_description = file: PACKAGE.md long_description_content_type = text/markdown url = https://scanoss.com project_urls = Source = https://github.com/scanoss/scanoss.py Tracker = https://github.com/scanoss/scanoss.py/issues - classifiers = Programming Language :: Python :: 3 - License :: OSI Approved :: MIT + License :: OSI Approved :: MIT License Operating System :: OS Independent + Development Status :: 5 - Production/Stable + Programming Language :: Python :: 3 [options] +packages = find_namespace: package_dir = = src -packages = find: -python_requires = >=3.7 +include_package_data = True +python_requires = >=3.9 +install_requires = + requests + crc32c>=2.2 + binaryornot + progress + grpcio>=1.73.1 + protobuf>=6.3.1 + protoc-gen-openapiv2 + pypac + pyOpenSSL + google-api-core + importlib_resources + packageurl-python + pathspec + jsonschema + crc + protoc-gen-openapiv2 + cyclonedx-python-lib[validation] + +[options.extras_require] +fast_winnowing = + scanoss_winnowing>=0.5.0 [options.packages.find] where = src +[options.package_data] +* = data/*.txt, data/*.json + [options.entry_points] console_scripts = scanoss-py = scanoss.cli:main \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 6d908fd5..00000000 --- a/setup.py +++ /dev/null @@ -1,39 +0,0 @@ -import codecs -import os -from setuptools import setup - - -def read(rel_path): - here = os.path.abspath(os.path.dirname(__file__)) - with codecs.open(os.path.join(here, rel_path), 'r') as fp: - return fp.read() - - -def get_version(rel_path): - for line in read(rel_path).splitlines(): - if line.startswith('__version__'): - delim = '"' if '"' in line else "'" - return line.split(delim)[1] - else: - raise RuntimeError("Unable to find version string.") - - -setup( - name="scanoss", - version=get_version("src/scanoss/__init__.py"), - author="SCANOSS", - author_email="info@scanoss.com", - license='MIT', - description='Simple Python library to use the SCANOSS APIs.', - long_description=read("PACKAGE.md"), - long_description_content_type='text/markdown', - install_requires=["requests", "crc32c>=2.2", "binaryornot", "progress", "grpcio<=1.42.0", "protobuf<=3.19.1"], - include_package_data=True, - package_data={'': ['data/*.json']}, - classifiers=[ - "Development Status :: 4 - Beta", - "Programming Language :: Python :: 3", - "Operating System :: OS Independent" - ], - python_requires='>=3.7' -) diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/protoc_gen_swagger/__init__.py b/src/protoc_gen_swagger/__init__.py new file mode 100644 index 00000000..6f3297a5 --- /dev/null +++ b/src/protoc_gen_swagger/__init__.py @@ -0,0 +1,20 @@ +""" +SPDX-License-Identifier: BSD-3-Clause + + Copyright (c) 2015, Gengo, Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Gengo, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. +""" diff --git a/src/protoc_gen_swagger/options/__init__.py b/src/protoc_gen_swagger/options/__init__.py new file mode 100644 index 00000000..6f3297a5 --- /dev/null +++ b/src/protoc_gen_swagger/options/__init__.py @@ -0,0 +1,20 @@ +""" +SPDX-License-Identifier: BSD-3-Clause + + Copyright (c) 2015, Gengo, Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Gengo, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. +""" diff --git a/src/protoc_gen_swagger/options/annotations_pb2.py b/src/protoc_gen_swagger/options/annotations_pb2.py new file mode 100644 index 00000000..d08ab680 --- /dev/null +++ b/src/protoc_gen_swagger/options/annotations_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: protoc-gen-swagger/options/annotations.proto +# Protobuf Python Version: 6.31.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 0, + '', + 'protoc-gen-swagger/options/annotations.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 +from protoc_gen_swagger.options import openapiv2_pb2 as protoc__gen__swagger_dot_options_dot_openapiv2__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,protoc-gen-swagger/options/annotations.proto\x12\'grpc.gateway.protoc_gen_swagger.options\x1a google/protobuf/descriptor.proto\x1a*protoc-gen-swagger/options/openapiv2.proto:j\n\x11openapiv2_swagger\x12\x1c.google.protobuf.FileOptions\x18\x92\x08 \x01(\x0b\x32\x30.grpc.gateway.protoc_gen_swagger.options.Swagger:p\n\x13openapiv2_operation\x12\x1e.google.protobuf.MethodOptions\x18\x92\x08 \x01(\x0b\x32\x32.grpc.gateway.protoc_gen_swagger.options.Operation:k\n\x10openapiv2_schema\x12\x1f.google.protobuf.MessageOptions\x18\x92\x08 \x01(\x0b\x32/.grpc.gateway.protoc_gen_swagger.options.Schema:e\n\ropenapiv2_tag\x12\x1f.google.protobuf.ServiceOptions\x18\x92\x08 \x01(\x0b\x32,.grpc.gateway.protoc_gen_swagger.options.Tag:l\n\x0fopenapiv2_field\x12\x1d.google.protobuf.FieldOptions\x18\x92\x08 \x01(\x0b\x32\x33.grpc.gateway.protoc_gen_swagger.options.JSONSchemaBCZAgithub.amrom.workers.dev/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/optionsb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'protoc_gen_swagger.options.annotations_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'ZAgithub.amrom.workers.dev/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options' +# @@protoc_insertion_point(module_scope) diff --git a/src/protoc_gen_swagger/options/annotations_pb2.pyi b/src/protoc_gen_swagger/options/annotations_pb2.pyi new file mode 100644 index 00000000..82a4bd56 --- /dev/null +++ b/src/protoc_gen_swagger/options/annotations_pb2.pyi @@ -0,0 +1,48 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import google.protobuf.descriptor +import google.protobuf.descriptor_pb2 +import google.protobuf.internal.extension_dict +import protoc_gen_swagger.options.openapiv2_pb2 + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +OPENAPIV2_SWAGGER_FIELD_NUMBER: builtins.int +OPENAPIV2_OPERATION_FIELD_NUMBER: builtins.int +OPENAPIV2_SCHEMA_FIELD_NUMBER: builtins.int +OPENAPIV2_TAG_FIELD_NUMBER: builtins.int +OPENAPIV2_FIELD_FIELD_NUMBER: builtins.int +openapiv2_swagger: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FileOptions, protoc_gen_swagger.options.openapiv2_pb2.Swagger] +"""ID assigned by protobuf-global-extension-registry@google.com for grpc-gateway project. + +All IDs are the same, as assigned. It is okay that they are the same, as they extend +different descriptor messages. +""" +openapiv2_operation: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.MethodOptions, protoc_gen_swagger.options.openapiv2_pb2.Operation] +"""ID assigned by protobuf-global-extension-registry@google.com for grpc-gateway project. + +All IDs are the same, as assigned. It is okay that they are the same, as they extend +different descriptor messages. +""" +openapiv2_schema: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.MessageOptions, protoc_gen_swagger.options.openapiv2_pb2.Schema] +"""ID assigned by protobuf-global-extension-registry@google.com for grpc-gateway project. + +All IDs are the same, as assigned. It is okay that they are the same, as they extend +different descriptor messages. +""" +openapiv2_tag: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.ServiceOptions, protoc_gen_swagger.options.openapiv2_pb2.Tag] +"""ID assigned by protobuf-global-extension-registry@google.com for grpc-gateway project. + +All IDs are the same, as assigned. It is okay that they are the same, as they extend +different descriptor messages. +""" +openapiv2_field: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, protoc_gen_swagger.options.openapiv2_pb2.JSONSchema] +"""ID assigned by protobuf-global-extension-registry@google.com for grpc-gateway project. + +All IDs are the same, as assigned. It is okay that they are the same, as they extend +different descriptor messages. +""" diff --git a/src/protoc_gen_swagger/options/annotations_pb2_grpc.py b/src/protoc_gen_swagger/options/annotations_pb2_grpc.py new file mode 100644 index 00000000..d1e1d60e --- /dev/null +++ b/src/protoc_gen_swagger/options/annotations_pb2_grpc.py @@ -0,0 +1,24 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + + +GRPC_GENERATED_VERSION = '1.73.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in protoc_gen_swagger/options/annotations_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) diff --git a/src/protoc_gen_swagger/options/openapiv2_pb2.py b/src/protoc_gen_swagger/options/openapiv2_pb2.py new file mode 100644 index 00000000..b0db8ffe --- /dev/null +++ b/src/protoc_gen_swagger/options/openapiv2_pb2.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: protoc-gen-swagger/options/openapiv2.proto +# Protobuf Python Version: 6.31.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 0, + '', + 'protoc-gen-swagger/options/openapiv2.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n*protoc-gen-swagger/options/openapiv2.proto\x12\'grpc.gateway.protoc_gen_swagger.options\x1a\x19google/protobuf/any.proto\x1a\x1cgoogle/protobuf/struct.proto\"\xa0\x07\n\x07Swagger\x12\x0f\n\x07swagger\x18\x01 \x01(\t\x12;\n\x04info\x18\x02 \x01(\x0b\x32-.grpc.gateway.protoc_gen_swagger.options.Info\x12\x0c\n\x04host\x18\x03 \x01(\t\x12\x11\n\tbase_path\x18\x04 \x01(\t\x12O\n\x07schemes\x18\x05 \x03(\x0e\x32>.grpc.gateway.protoc_gen_swagger.options.Swagger.SwaggerScheme\x12\x10\n\x08\x63onsumes\x18\x06 \x03(\t\x12\x10\n\x08produces\x18\x07 \x03(\t\x12R\n\tresponses\x18\n \x03(\x0b\x32?.grpc.gateway.protoc_gen_swagger.options.Swagger.ResponsesEntry\x12Z\n\x14security_definitions\x18\x0b \x01(\x0b\x32<.grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions\x12N\n\x08security\x18\x0c \x03(\x0b\x32<.grpc.gateway.protoc_gen_swagger.options.SecurityRequirement\x12U\n\rexternal_docs\x18\x0e \x01(\x0b\x32>.grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation\x12T\n\nextensions\x18\x0f \x03(\x0b\x32@.grpc.gateway.protoc_gen_swagger.options.Swagger.ExtensionsEntry\x1a\x63\n\x0eResponsesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12@\n\x05value\x18\x02 \x01(\x0b\x32\x31.grpc.gateway.protoc_gen_swagger.options.Response:\x02\x38\x01\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01\"B\n\rSwaggerScheme\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x08\n\x04HTTP\x10\x01\x12\t\n\x05HTTPS\x10\x02\x12\x06\n\x02WS\x10\x03\x12\x07\n\x03WSS\x10\x04J\x04\x08\x08\x10\tJ\x04\x08\t\x10\nJ\x04\x08\r\x10\x0e\"\xa9\x05\n\tOperation\x12\x0c\n\x04tags\x18\x01 \x03(\t\x12\x0f\n\x07summary\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12U\n\rexternal_docs\x18\x04 \x01(\x0b\x32>.grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation\x12\x14\n\x0coperation_id\x18\x05 \x01(\t\x12\x10\n\x08\x63onsumes\x18\x06 \x03(\t\x12\x10\n\x08produces\x18\x07 \x03(\t\x12T\n\tresponses\x18\t \x03(\x0b\x32\x41.grpc.gateway.protoc_gen_swagger.options.Operation.ResponsesEntry\x12\x0f\n\x07schemes\x18\n \x03(\t\x12\x12\n\ndeprecated\x18\x0b \x01(\x08\x12N\n\x08security\x18\x0c \x03(\x0b\x32<.grpc.gateway.protoc_gen_swagger.options.SecurityRequirement\x12V\n\nextensions\x18\r \x03(\x0b\x32\x42.grpc.gateway.protoc_gen_swagger.options.Operation.ExtensionsEntry\x1a\x63\n\x0eResponsesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12@\n\x05value\x18\x02 \x01(\x0b\x32\x31.grpc.gateway.protoc_gen_swagger.options.Response:\x02\x38\x01\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01J\x04\x08\x08\x10\t\"\xab\x01\n\x06Header\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0e\n\x06\x66ormat\x18\x03 \x01(\t\x12\x0f\n\x07\x64\x65\x66\x61ult\x18\x06 \x01(\t\x12\x0f\n\x07pattern\x18\r \x01(\tJ\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x07\x10\x08J\x04\x08\x08\x10\tJ\x04\x08\t\x10\nJ\x04\x08\n\x10\x0bJ\x04\x08\x0b\x10\x0cJ\x04\x08\x0c\x10\rJ\x04\x08\x0e\x10\x0fJ\x04\x08\x0f\x10\x10J\x04\x08\x10\x10\x11J\x04\x08\x11\x10\x12J\x04\x08\x12\x10\x13\"\xb8\x04\n\x08Response\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12?\n\x06schema\x18\x02 \x01(\x0b\x32/.grpc.gateway.protoc_gen_swagger.options.Schema\x12O\n\x07headers\x18\x03 \x03(\x0b\x32>.grpc.gateway.protoc_gen_swagger.options.Response.HeadersEntry\x12Q\n\x08\x65xamples\x18\x04 \x03(\x0b\x32?.grpc.gateway.protoc_gen_swagger.options.Response.ExamplesEntry\x12U\n\nextensions\x18\x05 \x03(\x0b\x32\x41.grpc.gateway.protoc_gen_swagger.options.Response.ExtensionsEntry\x1a_\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12>\n\x05value\x18\x02 \x01(\x0b\x32/.grpc.gateway.protoc_gen_swagger.options.Header:\x02\x38\x01\x1a/\n\rExamplesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01\"\xf9\x02\n\x04Info\x12\r\n\x05title\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x18\n\x10terms_of_service\x18\x03 \x01(\t\x12\x41\n\x07\x63ontact\x18\x04 \x01(\x0b\x32\x30.grpc.gateway.protoc_gen_swagger.options.Contact\x12\x41\n\x07license\x18\x05 \x01(\x0b\x32\x30.grpc.gateway.protoc_gen_swagger.options.License\x12\x0f\n\x07version\x18\x06 \x01(\t\x12Q\n\nextensions\x18\x07 \x03(\x0b\x32=.grpc.gateway.protoc_gen_swagger.options.Info.ExtensionsEntry\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01\"3\n\x07\x43ontact\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\"$\n\x07License\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\"9\n\x15\x45xternalDocumentation\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\"\x9c\x02\n\x06Schema\x12H\n\x0bjson_schema\x18\x01 \x01(\x0b\x32\x33.grpc.gateway.protoc_gen_swagger.options.JSONSchema\x12\x15\n\rdiscriminator\x18\x02 \x01(\t\x12\x11\n\tread_only\x18\x03 \x01(\x08\x12U\n\rexternal_docs\x18\x05 \x01(\x0b\x32>.grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation\x12)\n\x07\x65xample\x18\x06 \x01(\x0b\x32\x14.google.protobuf.AnyB\x02\x18\x01\x12\x16\n\x0e\x65xample_string\x18\x07 \x01(\tJ\x04\x08\x04\x10\x05\"\xe3\x05\n\nJSONSchema\x12\x0b\n\x03ref\x18\x03 \x01(\t\x12\r\n\x05title\x18\x05 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12\x0f\n\x07\x64\x65\x66\x61ult\x18\x07 \x01(\t\x12\x11\n\tread_only\x18\x08 \x01(\x08\x12\x0f\n\x07\x65xample\x18\t \x01(\t\x12\x13\n\x0bmultiple_of\x18\n \x01(\x01\x12\x0f\n\x07maximum\x18\x0b \x01(\x01\x12\x19\n\x11\x65xclusive_maximum\x18\x0c \x01(\x08\x12\x0f\n\x07minimum\x18\r \x01(\x01\x12\x19\n\x11\x65xclusive_minimum\x18\x0e \x01(\x08\x12\x12\n\nmax_length\x18\x0f \x01(\x04\x12\x12\n\nmin_length\x18\x10 \x01(\x04\x12\x0f\n\x07pattern\x18\x11 \x01(\t\x12\x11\n\tmax_items\x18\x14 \x01(\x04\x12\x11\n\tmin_items\x18\x15 \x01(\x04\x12\x14\n\x0cunique_items\x18\x16 \x01(\x08\x12\x16\n\x0emax_properties\x18\x18 \x01(\x04\x12\x16\n\x0emin_properties\x18\x19 \x01(\x04\x12\x10\n\x08required\x18\x1a \x03(\t\x12\r\n\x05\x61rray\x18\" \x03(\t\x12W\n\x04type\x18# \x03(\x0e\x32I.grpc.gateway.protoc_gen_swagger.options.JSONSchema.JSONSchemaSimpleTypes\x12\x0e\n\x06\x66ormat\x18$ \x01(\t\x12\x0c\n\x04\x65num\x18. \x03(\t\"w\n\x15JSONSchemaSimpleTypes\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05\x41RRAY\x10\x01\x12\x0b\n\x07\x42OOLEAN\x10\x02\x12\x0b\n\x07INTEGER\x10\x03\x12\x08\n\x04NULL\x10\x04\x12\n\n\x06NUMBER\x10\x05\x12\n\n\x06OBJECT\x10\x06\x12\n\n\x06STRING\x10\x07J\x04\x08\x01\x10\x02J\x04\x08\x02\x10\x03J\x04\x08\x04\x10\x05J\x04\x08\x12\x10\x13J\x04\x08\x13\x10\x14J\x04\x08\x17\x10\x18J\x04\x08\x1b\x10\x1cJ\x04\x08\x1c\x10\x1dJ\x04\x08\x1d\x10\x1eJ\x04\x08\x1e\x10\"J\x04\x08%\x10*J\x04\x08*\x10+J\x04\x08+\x10.\"w\n\x03Tag\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12U\n\rexternal_docs\x18\x03 \x01(\x0b\x32>.grpc.gateway.protoc_gen_swagger.options.ExternalDocumentationJ\x04\x08\x01\x10\x02\"\xdd\x01\n\x13SecurityDefinitions\x12\\\n\x08security\x18\x01 \x03(\x0b\x32J.grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions.SecurityEntry\x1ah\n\rSecurityEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x46\n\x05value\x18\x02 \x01(\x0b\x32\x37.grpc.gateway.protoc_gen_swagger.options.SecurityScheme:\x02\x38\x01\"\x96\x06\n\x0eSecurityScheme\x12J\n\x04type\x18\x01 \x01(\x0e\x32<.grpc.gateway.protoc_gen_swagger.options.SecurityScheme.Type\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x46\n\x02in\x18\x04 \x01(\x0e\x32:.grpc.gateway.protoc_gen_swagger.options.SecurityScheme.In\x12J\n\x04\x66low\x18\x05 \x01(\x0e\x32<.grpc.gateway.protoc_gen_swagger.options.SecurityScheme.Flow\x12\x19\n\x11\x61uthorization_url\x18\x06 \x01(\t\x12\x11\n\ttoken_url\x18\x07 \x01(\t\x12?\n\x06scopes\x18\x08 \x01(\x0b\x32/.grpc.gateway.protoc_gen_swagger.options.Scopes\x12[\n\nextensions\x18\t \x03(\x0b\x32G.grpc.gateway.protoc_gen_swagger.options.SecurityScheme.ExtensionsEntry\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01\"K\n\x04Type\x12\x10\n\x0cTYPE_INVALID\x10\x00\x12\x0e\n\nTYPE_BASIC\x10\x01\x12\x10\n\x0cTYPE_API_KEY\x10\x02\x12\x0f\n\x0bTYPE_OAUTH2\x10\x03\"1\n\x02In\x12\x0e\n\nIN_INVALID\x10\x00\x12\x0c\n\x08IN_QUERY\x10\x01\x12\r\n\tIN_HEADER\x10\x02\"j\n\x04\x46low\x12\x10\n\x0c\x46LOW_INVALID\x10\x00\x12\x11\n\rFLOW_IMPLICIT\x10\x01\x12\x11\n\rFLOW_PASSWORD\x10\x02\x12\x14\n\x10\x46LOW_APPLICATION\x10\x03\x12\x14\n\x10\x46LOW_ACCESS_CODE\x10\x04\"\xc9\x02\n\x13SecurityRequirement\x12s\n\x14security_requirement\x18\x01 \x03(\x0b\x32U.grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementEntry\x1a)\n\x18SecurityRequirementValue\x12\r\n\x05scope\x18\x01 \x03(\t\x1a\x91\x01\n\x18SecurityRequirementEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x64\n\x05value\x18\x02 \x01(\x0b\x32U.grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementValue:\x02\x38\x01\"\x81\x01\n\x06Scopes\x12I\n\x05scope\x18\x01 \x03(\x0b\x32:.grpc.gateway.protoc_gen_swagger.options.Scopes.ScopeEntry\x1a,\n\nScopeEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x43ZAgithub.amrom.workers.dev/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/optionsb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'protoc_gen_swagger.options.openapiv2_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'ZAgithub.amrom.workers.dev/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options' + _globals['_SWAGGER_RESPONSESENTRY']._loaded_options = None + _globals['_SWAGGER_RESPONSESENTRY']._serialized_options = b'8\001' + _globals['_SWAGGER_EXTENSIONSENTRY']._loaded_options = None + _globals['_SWAGGER_EXTENSIONSENTRY']._serialized_options = b'8\001' + _globals['_OPERATION_RESPONSESENTRY']._loaded_options = None + _globals['_OPERATION_RESPONSESENTRY']._serialized_options = b'8\001' + _globals['_OPERATION_EXTENSIONSENTRY']._loaded_options = None + _globals['_OPERATION_EXTENSIONSENTRY']._serialized_options = b'8\001' + _globals['_RESPONSE_HEADERSENTRY']._loaded_options = None + _globals['_RESPONSE_HEADERSENTRY']._serialized_options = b'8\001' + _globals['_RESPONSE_EXAMPLESENTRY']._loaded_options = None + _globals['_RESPONSE_EXAMPLESENTRY']._serialized_options = b'8\001' + _globals['_RESPONSE_EXTENSIONSENTRY']._loaded_options = None + _globals['_RESPONSE_EXTENSIONSENTRY']._serialized_options = b'8\001' + _globals['_INFO_EXTENSIONSENTRY']._loaded_options = None + _globals['_INFO_EXTENSIONSENTRY']._serialized_options = b'8\001' + _globals['_SCHEMA'].fields_by_name['example']._loaded_options = None + _globals['_SCHEMA'].fields_by_name['example']._serialized_options = b'\030\001' + _globals['_SECURITYDEFINITIONS_SECURITYENTRY']._loaded_options = None + _globals['_SECURITYDEFINITIONS_SECURITYENTRY']._serialized_options = b'8\001' + _globals['_SECURITYSCHEME_EXTENSIONSENTRY']._loaded_options = None + _globals['_SECURITYSCHEME_EXTENSIONSENTRY']._serialized_options = b'8\001' + _globals['_SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY']._loaded_options = None + _globals['_SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY']._serialized_options = b'8\001' + _globals['_SCOPES_SCOPEENTRY']._loaded_options = None + _globals['_SCOPES_SCOPEENTRY']._serialized_options = b'8\001' + _globals['_SWAGGER']._serialized_start=145 + _globals['_SWAGGER']._serialized_end=1073 + _globals['_SWAGGER_RESPONSESENTRY']._serialized_start=813 + _globals['_SWAGGER_RESPONSESENTRY']._serialized_end=912 + _globals['_SWAGGER_EXTENSIONSENTRY']._serialized_start=914 + _globals['_SWAGGER_EXTENSIONSENTRY']._serialized_end=987 + _globals['_SWAGGER_SWAGGERSCHEME']._serialized_start=989 + _globals['_SWAGGER_SWAGGERSCHEME']._serialized_end=1055 + _globals['_OPERATION']._serialized_start=1076 + _globals['_OPERATION']._serialized_end=1757 + _globals['_OPERATION_RESPONSESENTRY']._serialized_start=813 + _globals['_OPERATION_RESPONSESENTRY']._serialized_end=912 + _globals['_OPERATION_EXTENSIONSENTRY']._serialized_start=914 + _globals['_OPERATION_EXTENSIONSENTRY']._serialized_end=987 + _globals['_HEADER']._serialized_start=1760 + _globals['_HEADER']._serialized_end=1931 + _globals['_RESPONSE']._serialized_start=1934 + _globals['_RESPONSE']._serialized_end=2502 + _globals['_RESPONSE_HEADERSENTRY']._serialized_start=2283 + _globals['_RESPONSE_HEADERSENTRY']._serialized_end=2378 + _globals['_RESPONSE_EXAMPLESENTRY']._serialized_start=2380 + _globals['_RESPONSE_EXAMPLESENTRY']._serialized_end=2427 + _globals['_RESPONSE_EXTENSIONSENTRY']._serialized_start=914 + _globals['_RESPONSE_EXTENSIONSENTRY']._serialized_end=987 + _globals['_INFO']._serialized_start=2505 + _globals['_INFO']._serialized_end=2882 + _globals['_INFO_EXTENSIONSENTRY']._serialized_start=914 + _globals['_INFO_EXTENSIONSENTRY']._serialized_end=987 + _globals['_CONTACT']._serialized_start=2884 + _globals['_CONTACT']._serialized_end=2935 + _globals['_LICENSE']._serialized_start=2937 + _globals['_LICENSE']._serialized_end=2973 + _globals['_EXTERNALDOCUMENTATION']._serialized_start=2975 + _globals['_EXTERNALDOCUMENTATION']._serialized_end=3032 + _globals['_SCHEMA']._serialized_start=3035 + _globals['_SCHEMA']._serialized_end=3319 + _globals['_JSONSCHEMA']._serialized_start=3322 + _globals['_JSONSCHEMA']._serialized_end=4061 + _globals['_JSONSCHEMA_JSONSCHEMASIMPLETYPES']._serialized_start=3864 + _globals['_JSONSCHEMA_JSONSCHEMASIMPLETYPES']._serialized_end=3983 + _globals['_TAG']._serialized_start=4063 + _globals['_TAG']._serialized_end=4182 + _globals['_SECURITYDEFINITIONS']._serialized_start=4185 + _globals['_SECURITYDEFINITIONS']._serialized_end=4406 + _globals['_SECURITYDEFINITIONS_SECURITYENTRY']._serialized_start=4302 + _globals['_SECURITYDEFINITIONS_SECURITYENTRY']._serialized_end=4406 + _globals['_SECURITYSCHEME']._serialized_start=4409 + _globals['_SECURITYSCHEME']._serialized_end=5199 + _globals['_SECURITYSCHEME_EXTENSIONSENTRY']._serialized_start=914 + _globals['_SECURITYSCHEME_EXTENSIONSENTRY']._serialized_end=987 + _globals['_SECURITYSCHEME_TYPE']._serialized_start=4965 + _globals['_SECURITYSCHEME_TYPE']._serialized_end=5040 + _globals['_SECURITYSCHEME_IN']._serialized_start=5042 + _globals['_SECURITYSCHEME_IN']._serialized_end=5091 + _globals['_SECURITYSCHEME_FLOW']._serialized_start=5093 + _globals['_SECURITYSCHEME_FLOW']._serialized_end=5199 + _globals['_SECURITYREQUIREMENT']._serialized_start=5202 + _globals['_SECURITYREQUIREMENT']._serialized_end=5531 + _globals['_SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE']._serialized_start=5342 + _globals['_SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE']._serialized_end=5383 + _globals['_SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY']._serialized_start=5386 + _globals['_SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY']._serialized_end=5531 + _globals['_SCOPES']._serialized_start=5534 + _globals['_SCOPES']._serialized_end=5663 + _globals['_SCOPES_SCOPEENTRY']._serialized_start=5619 + _globals['_SCOPES_SCOPEENTRY']._serialized_end=5663 +# @@protoc_insertion_point(module_scope) diff --git a/src/protoc_gen_swagger/options/openapiv2_pb2.pyi b/src/protoc_gen_swagger/options/openapiv2_pb2.pyi new file mode 100644 index 00000000..b8bfbaf0 --- /dev/null +++ b/src/protoc_gen_swagger/options/openapiv2_pb2.pyi @@ -0,0 +1,1317 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import collections.abc +import google.protobuf.any_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import google.protobuf.struct_pb2 +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +@typing.final +class Swagger(google.protobuf.message.Message): + """`Swagger` is a representation of OpenAPI v2 specification's Swagger object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#swaggerObject + + Example: + + option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { + info: { + title: "Echo API"; + version: "1.0"; + description: "; + contact: { + name: "gRPC-Gateway project"; + url: "https://github.com/grpc-ecosystem/grpc-gateway"; + email: "none@example.com"; + }; + license: { + name: "BSD 3-Clause License"; + url: "https://github.com/grpc-ecosystem/grpc-gateway/blob/master/LICENSE.txt"; + }; + }; + schemes: HTTPS; + consumes: "application/json"; + produces: "application/json"; + }; + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _SwaggerScheme: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _SwaggerSchemeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Swagger._SwaggerScheme.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + UNKNOWN: Swagger._SwaggerScheme.ValueType # 0 + HTTP: Swagger._SwaggerScheme.ValueType # 1 + HTTPS: Swagger._SwaggerScheme.ValueType # 2 + WS: Swagger._SwaggerScheme.ValueType # 3 + WSS: Swagger._SwaggerScheme.ValueType # 4 + + class SwaggerScheme(_SwaggerScheme, metaclass=_SwaggerSchemeEnumTypeWrapper): ... + UNKNOWN: Swagger.SwaggerScheme.ValueType # 0 + HTTP: Swagger.SwaggerScheme.ValueType # 1 + HTTPS: Swagger.SwaggerScheme.ValueType # 2 + WS: Swagger.SwaggerScheme.ValueType # 3 + WSS: Swagger.SwaggerScheme.ValueType # 4 + + @typing.final + class ResponsesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___Response: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___Response | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + @typing.final + class ExtensionsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> google.protobuf.struct_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: google.protobuf.struct_pb2.Value | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + SWAGGER_FIELD_NUMBER: builtins.int + INFO_FIELD_NUMBER: builtins.int + HOST_FIELD_NUMBER: builtins.int + BASE_PATH_FIELD_NUMBER: builtins.int + SCHEMES_FIELD_NUMBER: builtins.int + CONSUMES_FIELD_NUMBER: builtins.int + PRODUCES_FIELD_NUMBER: builtins.int + RESPONSES_FIELD_NUMBER: builtins.int + SECURITY_DEFINITIONS_FIELD_NUMBER: builtins.int + SECURITY_FIELD_NUMBER: builtins.int + EXTERNAL_DOCS_FIELD_NUMBER: builtins.int + EXTENSIONS_FIELD_NUMBER: builtins.int + swagger: builtins.str + """Specifies the Swagger Specification version being used. It can be + used by the Swagger UI and other clients to interpret the API listing. The + value MUST be "2.0". + """ + host: builtins.str + """The host (name or ip) serving the API. This MUST be the host only and does + not include the scheme nor sub-paths. It MAY include a port. If the host is + not included, the host serving the documentation is to be used (including + the port). The host does not support path templating. + """ + base_path: builtins.str + """The base path on which the API is served, which is relative to the host. If + it is not included, the API is served directly under the host. The value + MUST start with a leading slash (/). The basePath does not support path + templating. + Note that using `base_path` does not change the endpoint paths that are + generated in the resulting Swagger file. If you wish to use `base_path` + with relatively generated Swagger paths, the `base_path` prefix must be + manually removed from your `google.api.http` paths and your code changed to + serve the API from the `base_path`. + """ + @property + def info(self) -> global___Info: + """Provides metadata about the API. The metadata can be used by the + clients if needed. + """ + + @property + def schemes(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___Swagger.SwaggerScheme.ValueType]: + """The transfer protocol of the API. Values MUST be from the list: "http", + "https", "ws", "wss". If the schemes is not included, the default scheme to + be used is the one used to access the Swagger definition itself. + """ + + @property + def consumes(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """A list of MIME types the APIs can consume. This is global to all APIs but + can be overridden on specific API calls. Value MUST be as described under + Mime Types. + """ + + @property + def produces(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """A list of MIME types the APIs can produce. This is global to all APIs but + can be overridden on specific API calls. Value MUST be as described under + Mime Types. + """ + + @property + def responses(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___Response]: + """An object to hold responses that can be used across operations. This + property does not define global responses for all operations. + """ + + @property + def security_definitions(self) -> global___SecurityDefinitions: + """Security scheme definitions that can be used across the specification.""" + + @property + def security(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___SecurityRequirement]: + """A declaration of which security schemes are applied for the API as a whole. + The list of values describes alternative security schemes that can be used + (that is, there is a logical OR between the security requirements). + Individual operations can override this definition. + """ + + @property + def external_docs(self) -> global___ExternalDocumentation: + """Additional external documentation.""" + + @property + def extensions(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, google.protobuf.struct_pb2.Value]: ... + def __init__( + self, + *, + swagger: builtins.str = ..., + info: global___Info | None = ..., + host: builtins.str = ..., + base_path: builtins.str = ..., + schemes: collections.abc.Iterable[global___Swagger.SwaggerScheme.ValueType] | None = ..., + consumes: collections.abc.Iterable[builtins.str] | None = ..., + produces: collections.abc.Iterable[builtins.str] | None = ..., + responses: collections.abc.Mapping[builtins.str, global___Response] | None = ..., + security_definitions: global___SecurityDefinitions | None = ..., + security: collections.abc.Iterable[global___SecurityRequirement] | None = ..., + external_docs: global___ExternalDocumentation | None = ..., + extensions: collections.abc.Mapping[builtins.str, google.protobuf.struct_pb2.Value] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["external_docs", b"external_docs", "info", b"info", "security_definitions", b"security_definitions"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["base_path", b"base_path", "consumes", b"consumes", "extensions", b"extensions", "external_docs", b"external_docs", "host", b"host", "info", b"info", "produces", b"produces", "responses", b"responses", "schemes", b"schemes", "security", b"security", "security_definitions", b"security_definitions", "swagger", b"swagger"]) -> None: ... + +global___Swagger = Swagger + +@typing.final +class Operation(google.protobuf.message.Message): + """`Operation` is a representation of OpenAPI v2 specification's Operation object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#operationObject + + Example: + + service EchoService { + rpc Echo(SimpleMessage) returns (SimpleMessage) { + option (google.api.http) = { + get: "/v1/example/echo/{id}" + }; + + option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { + summary: "Get a message."; + operation_id: "getMessage"; + tags: "echo"; + responses: { + key: "200" + value: { + description: "OK"; + } + } + }; + } + } + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing.final + class ResponsesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___Response: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___Response | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + @typing.final + class ExtensionsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> google.protobuf.struct_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: google.protobuf.struct_pb2.Value | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + TAGS_FIELD_NUMBER: builtins.int + SUMMARY_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + EXTERNAL_DOCS_FIELD_NUMBER: builtins.int + OPERATION_ID_FIELD_NUMBER: builtins.int + CONSUMES_FIELD_NUMBER: builtins.int + PRODUCES_FIELD_NUMBER: builtins.int + RESPONSES_FIELD_NUMBER: builtins.int + SCHEMES_FIELD_NUMBER: builtins.int + DEPRECATED_FIELD_NUMBER: builtins.int + SECURITY_FIELD_NUMBER: builtins.int + EXTENSIONS_FIELD_NUMBER: builtins.int + summary: builtins.str + """A short summary of what the operation does. For maximum readability in the + swagger-ui, this field SHOULD be less than 120 characters. + """ + description: builtins.str + """A verbose explanation of the operation behavior. GFM syntax can be used for + rich text representation. + """ + operation_id: builtins.str + """Unique string used to identify the operation. The id MUST be unique among + all operations described in the API. Tools and libraries MAY use the + operationId to uniquely identify an operation, therefore, it is recommended + to follow common programming naming conventions. + """ + deprecated: builtins.bool + """Declares this operation to be deprecated. Usage of the declared operation + should be refrained. Default value is false. + """ + @property + def tags(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """A list of tags for API documentation control. Tags can be used for logical + grouping of operations by resources or any other qualifier. + """ + + @property + def external_docs(self) -> global___ExternalDocumentation: + """Additional external documentation for this operation.""" + + @property + def consumes(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """A list of MIME types the operation can consume. This overrides the consumes + definition at the Swagger Object. An empty value MAY be used to clear the + global definition. Value MUST be as described under Mime Types. + """ + + @property + def produces(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """A list of MIME types the operation can produce. This overrides the produces + definition at the Swagger Object. An empty value MAY be used to clear the + global definition. Value MUST be as described under Mime Types. + """ + + @property + def responses(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___Response]: + """The list of possible responses as they are returned from executing this + operation. + """ + + @property + def schemes(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """The transfer protocol for the operation. Values MUST be from the list: + "http", "https", "ws", "wss". The value overrides the Swagger Object + schemes definition. + """ + + @property + def security(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___SecurityRequirement]: + """A declaration of which security schemes are applied for this operation. The + list of values describes alternative security schemes that can be used + (that is, there is a logical OR between the security requirements). This + definition overrides any declared top-level security. To remove a top-level + security declaration, an empty array can be used. + """ + + @property + def extensions(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, google.protobuf.struct_pb2.Value]: ... + def __init__( + self, + *, + tags: collections.abc.Iterable[builtins.str] | None = ..., + summary: builtins.str = ..., + description: builtins.str = ..., + external_docs: global___ExternalDocumentation | None = ..., + operation_id: builtins.str = ..., + consumes: collections.abc.Iterable[builtins.str] | None = ..., + produces: collections.abc.Iterable[builtins.str] | None = ..., + responses: collections.abc.Mapping[builtins.str, global___Response] | None = ..., + schemes: collections.abc.Iterable[builtins.str] | None = ..., + deprecated: builtins.bool = ..., + security: collections.abc.Iterable[global___SecurityRequirement] | None = ..., + extensions: collections.abc.Mapping[builtins.str, google.protobuf.struct_pb2.Value] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["external_docs", b"external_docs"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["consumes", b"consumes", "deprecated", b"deprecated", "description", b"description", "extensions", b"extensions", "external_docs", b"external_docs", "operation_id", b"operation_id", "produces", b"produces", "responses", b"responses", "schemes", b"schemes", "security", b"security", "summary", b"summary", "tags", b"tags"]) -> None: ... + +global___Operation = Operation + +@typing.final +class Header(google.protobuf.message.Message): + """`Header` is a representation of OpenAPI v2 specification's Header object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#headerObject + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DESCRIPTION_FIELD_NUMBER: builtins.int + TYPE_FIELD_NUMBER: builtins.int + FORMAT_FIELD_NUMBER: builtins.int + DEFAULT_FIELD_NUMBER: builtins.int + PATTERN_FIELD_NUMBER: builtins.int + description: builtins.str + """`Description` is a short description of the header.""" + type: builtins.str + """The type of the object. The value MUST be one of "string", "number", "integer", or "boolean". The "array" type is not supported.""" + format: builtins.str + """`Format` The extending format for the previously mentioned type.""" + default: builtins.str + """`Default` Declares the value of the header that the server will use if none is provided. + See: https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-6.2. + Unlike JSON Schema this value MUST conform to the defined type for the header. + """ + pattern: builtins.str + """'Pattern' See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.3.""" + def __init__( + self, + *, + description: builtins.str = ..., + type: builtins.str = ..., + format: builtins.str = ..., + default: builtins.str = ..., + pattern: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["default", b"default", "description", b"description", "format", b"format", "pattern", b"pattern", "type", b"type"]) -> None: ... + +global___Header = Header + +@typing.final +class Response(google.protobuf.message.Message): + """`Response` is a representation of OpenAPI v2 specification's Response object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#responseObject + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing.final + class HeadersEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___Header: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___Header | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + @typing.final + class ExamplesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + @typing.final + class ExtensionsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> google.protobuf.struct_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: google.protobuf.struct_pb2.Value | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + DESCRIPTION_FIELD_NUMBER: builtins.int + SCHEMA_FIELD_NUMBER: builtins.int + HEADERS_FIELD_NUMBER: builtins.int + EXAMPLES_FIELD_NUMBER: builtins.int + EXTENSIONS_FIELD_NUMBER: builtins.int + description: builtins.str + """`Description` is a short description of the response. + GFM syntax can be used for rich text representation. + """ + @property + def schema(self) -> global___Schema: + """`Schema` optionally defines the structure of the response. + If `Schema` is not provided, it means there is no content to the response. + """ + + @property + def headers(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___Header]: + """`Headers` A list of headers that are sent with the response. + `Header` name is expected to be a string in the canonical format of the MIME header key + See: https://golang.org/pkg/net/textproto/#CanonicalMIMEHeaderKey + """ + + @property + def examples(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """`Examples` gives per-mimetype response examples. + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#example-object + """ + + @property + def extensions(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, google.protobuf.struct_pb2.Value]: ... + def __init__( + self, + *, + description: builtins.str = ..., + schema: global___Schema | None = ..., + headers: collections.abc.Mapping[builtins.str, global___Header] | None = ..., + examples: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + extensions: collections.abc.Mapping[builtins.str, google.protobuf.struct_pb2.Value] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["schema", b"schema"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["description", b"description", "examples", b"examples", "extensions", b"extensions", "headers", b"headers", "schema", b"schema"]) -> None: ... + +global___Response = Response + +@typing.final +class Info(google.protobuf.message.Message): + """`Info` is a representation of OpenAPI v2 specification's Info object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#infoObject + + Example: + + option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { + info: { + title: "Echo API"; + version: "1.0"; + description: "; + contact: { + name: "gRPC-Gateway project"; + url: "https://github.com/grpc-ecosystem/grpc-gateway"; + email: "none@example.com"; + }; + license: { + name: "BSD 3-Clause License"; + url: "https://github.com/grpc-ecosystem/grpc-gateway/blob/master/LICENSE.txt"; + }; + }; + ... + }; + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing.final + class ExtensionsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> google.protobuf.struct_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: google.protobuf.struct_pb2.Value | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + TITLE_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + TERMS_OF_SERVICE_FIELD_NUMBER: builtins.int + CONTACT_FIELD_NUMBER: builtins.int + LICENSE_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + EXTENSIONS_FIELD_NUMBER: builtins.int + title: builtins.str + """The title of the application.""" + description: builtins.str + """A short description of the application. GFM syntax can be used for rich + text representation. + """ + terms_of_service: builtins.str + """The Terms of Service for the API.""" + version: builtins.str + """Provides the version of the application API (not to be confused + with the specification version). + """ + @property + def contact(self) -> global___Contact: + """The contact information for the exposed API.""" + + @property + def license(self) -> global___License: + """The license information for the exposed API.""" + + @property + def extensions(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, google.protobuf.struct_pb2.Value]: ... + def __init__( + self, + *, + title: builtins.str = ..., + description: builtins.str = ..., + terms_of_service: builtins.str = ..., + contact: global___Contact | None = ..., + license: global___License | None = ..., + version: builtins.str = ..., + extensions: collections.abc.Mapping[builtins.str, google.protobuf.struct_pb2.Value] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["contact", b"contact", "license", b"license"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["contact", b"contact", "description", b"description", "extensions", b"extensions", "license", b"license", "terms_of_service", b"terms_of_service", "title", b"title", "version", b"version"]) -> None: ... + +global___Info = Info + +@typing.final +class Contact(google.protobuf.message.Message): + """`Contact` is a representation of OpenAPI v2 specification's Contact object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#contactObject + + Example: + + option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { + info: { + ... + contact: { + name: "gRPC-Gateway project"; + url: "https://github.com/grpc-ecosystem/grpc-gateway"; + email: "none@example.com"; + }; + ... + }; + ... + }; + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + URL_FIELD_NUMBER: builtins.int + EMAIL_FIELD_NUMBER: builtins.int + name: builtins.str + """The identifying name of the contact person/organization.""" + url: builtins.str + """The URL pointing to the contact information. MUST be in the format of a + URL. + """ + email: builtins.str + """The email address of the contact person/organization. MUST be in the format + of an email address. + """ + def __init__( + self, + *, + name: builtins.str = ..., + url: builtins.str = ..., + email: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["email", b"email", "name", b"name", "url", b"url"]) -> None: ... + +global___Contact = Contact + +@typing.final +class License(google.protobuf.message.Message): + """`License` is a representation of OpenAPI v2 specification's License object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#licenseObject + + Example: + + option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { + info: { + ... + license: { + name: "BSD 3-Clause License"; + url: "https://github.com/grpc-ecosystem/grpc-gateway/blob/master/LICENSE.txt"; + }; + ... + }; + ... + }; + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + URL_FIELD_NUMBER: builtins.int + name: builtins.str + """The license name used for the API.""" + url: builtins.str + """A URL to the license used for the API. MUST be in the format of a URL.""" + def __init__( + self, + *, + name: builtins.str = ..., + url: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["name", b"name", "url", b"url"]) -> None: ... + +global___License = License + +@typing.final +class ExternalDocumentation(google.protobuf.message.Message): + """`ExternalDocumentation` is a representation of OpenAPI v2 specification's + ExternalDocumentation object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#externalDocumentationObject + + Example: + + option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { + ... + external_docs: { + description: "More about gRPC-Gateway"; + url: "https://github.com/grpc-ecosystem/grpc-gateway"; + } + ... + }; + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DESCRIPTION_FIELD_NUMBER: builtins.int + URL_FIELD_NUMBER: builtins.int + description: builtins.str + """A short description of the target documentation. GFM syntax can be used for + rich text representation. + """ + url: builtins.str + """The URL for the target documentation. Value MUST be in the format + of a URL. + """ + def __init__( + self, + *, + description: builtins.str = ..., + url: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["description", b"description", "url", b"url"]) -> None: ... + +global___ExternalDocumentation = ExternalDocumentation + +@typing.final +class Schema(google.protobuf.message.Message): + """`Schema` is a representation of OpenAPI v2 specification's Schema object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#schemaObject + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + JSON_SCHEMA_FIELD_NUMBER: builtins.int + DISCRIMINATOR_FIELD_NUMBER: builtins.int + READ_ONLY_FIELD_NUMBER: builtins.int + EXTERNAL_DOCS_FIELD_NUMBER: builtins.int + EXAMPLE_FIELD_NUMBER: builtins.int + EXAMPLE_STRING_FIELD_NUMBER: builtins.int + discriminator: builtins.str + """Adds support for polymorphism. The discriminator is the schema property + name that is used to differentiate between other schema that inherit this + schema. The property name used MUST be defined at this schema and it MUST + be in the required property list. When used, the value MUST be the name of + this schema or any schema that inherits it. + """ + read_only: builtins.bool + """Relevant only for Schema "properties" definitions. Declares the property as + "read only". This means that it MAY be sent as part of a response but MUST + NOT be sent as part of the request. Properties marked as readOnly being + true SHOULD NOT be in the required list of the defined schema. Default + value is false. + """ + example_string: builtins.str + """A free-form property to include a JSON example of this field. This is copied + verbatim to the output swagger.json. Quotes must be escaped. + """ + @property + def json_schema(self) -> global___JSONSchema: ... + @property + def external_docs(self) -> global___ExternalDocumentation: + """Additional external documentation for this schema.""" + + @property + def example(self) -> google.protobuf.any_pb2.Any: + """A free-form property to include an example of an instance for this schema. + Deprecated, please use example_string instead. + """ + + def __init__( + self, + *, + json_schema: global___JSONSchema | None = ..., + discriminator: builtins.str = ..., + read_only: builtins.bool = ..., + external_docs: global___ExternalDocumentation | None = ..., + example: google.protobuf.any_pb2.Any | None = ..., + example_string: builtins.str = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["example", b"example", "external_docs", b"external_docs", "json_schema", b"json_schema"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["discriminator", b"discriminator", "example", b"example", "example_string", b"example_string", "external_docs", b"external_docs", "json_schema", b"json_schema", "read_only", b"read_only"]) -> None: ... + +global___Schema = Schema + +@typing.final +class JSONSchema(google.protobuf.message.Message): + """`JSONSchema` represents properties from JSON Schema taken, and as used, in + the OpenAPI v2 spec. + + This includes changes made by OpenAPI v2. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#schemaObject + + See also: https://cswr.github.io/JsonSchema/spec/basic_types/, + https://github.com/json-schema-org/json-schema-spec/blob/master/schema.json + + Example: + + message SimpleMessage { + option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { + json_schema: { + title: "SimpleMessage" + description: "A simple message." + required: ["id"] + } + }; + + // Id represents the message identifier. + string id = 1; [ + (grpc.gateway.protoc_gen_swagger.options.openapiv2_field) = { + {description: "The unique identifier of the simple message." + }]; + } + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _JSONSchemaSimpleTypes: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _JSONSchemaSimpleTypesEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[JSONSchema._JSONSchemaSimpleTypes.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + UNKNOWN: JSONSchema._JSONSchemaSimpleTypes.ValueType # 0 + ARRAY: JSONSchema._JSONSchemaSimpleTypes.ValueType # 1 + BOOLEAN: JSONSchema._JSONSchemaSimpleTypes.ValueType # 2 + INTEGER: JSONSchema._JSONSchemaSimpleTypes.ValueType # 3 + NULL: JSONSchema._JSONSchemaSimpleTypes.ValueType # 4 + NUMBER: JSONSchema._JSONSchemaSimpleTypes.ValueType # 5 + OBJECT: JSONSchema._JSONSchemaSimpleTypes.ValueType # 6 + STRING: JSONSchema._JSONSchemaSimpleTypes.ValueType # 7 + + class JSONSchemaSimpleTypes(_JSONSchemaSimpleTypes, metaclass=_JSONSchemaSimpleTypesEnumTypeWrapper): ... + UNKNOWN: JSONSchema.JSONSchemaSimpleTypes.ValueType # 0 + ARRAY: JSONSchema.JSONSchemaSimpleTypes.ValueType # 1 + BOOLEAN: JSONSchema.JSONSchemaSimpleTypes.ValueType # 2 + INTEGER: JSONSchema.JSONSchemaSimpleTypes.ValueType # 3 + NULL: JSONSchema.JSONSchemaSimpleTypes.ValueType # 4 + NUMBER: JSONSchema.JSONSchemaSimpleTypes.ValueType # 5 + OBJECT: JSONSchema.JSONSchemaSimpleTypes.ValueType # 6 + STRING: JSONSchema.JSONSchemaSimpleTypes.ValueType # 7 + + REF_FIELD_NUMBER: builtins.int + TITLE_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + DEFAULT_FIELD_NUMBER: builtins.int + READ_ONLY_FIELD_NUMBER: builtins.int + EXAMPLE_FIELD_NUMBER: builtins.int + MULTIPLE_OF_FIELD_NUMBER: builtins.int + MAXIMUM_FIELD_NUMBER: builtins.int + EXCLUSIVE_MAXIMUM_FIELD_NUMBER: builtins.int + MINIMUM_FIELD_NUMBER: builtins.int + EXCLUSIVE_MINIMUM_FIELD_NUMBER: builtins.int + MAX_LENGTH_FIELD_NUMBER: builtins.int + MIN_LENGTH_FIELD_NUMBER: builtins.int + PATTERN_FIELD_NUMBER: builtins.int + MAX_ITEMS_FIELD_NUMBER: builtins.int + MIN_ITEMS_FIELD_NUMBER: builtins.int + UNIQUE_ITEMS_FIELD_NUMBER: builtins.int + MAX_PROPERTIES_FIELD_NUMBER: builtins.int + MIN_PROPERTIES_FIELD_NUMBER: builtins.int + REQUIRED_FIELD_NUMBER: builtins.int + ARRAY_FIELD_NUMBER: builtins.int + TYPE_FIELD_NUMBER: builtins.int + FORMAT_FIELD_NUMBER: builtins.int + ENUM_FIELD_NUMBER: builtins.int + ref: builtins.str + """Ref is used to define an external reference to include in the message. + This could be a fully qualified proto message reference, and that type must + be imported into the protofile. If no message is identified, the Ref will + be used verbatim in the output. + For example: + `ref: ".google.protobuf.Timestamp"`. + """ + title: builtins.str + """The title of the schema.""" + description: builtins.str + """A short description of the schema.""" + default: builtins.str + read_only: builtins.bool + example: builtins.str + """A free-form property to include a JSON example of this field. This is copied + verbatim to the output swagger.json. Quotes must be escaped. + This property is the same for 2.0 and 3.0.0 https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/3.0.0.md#schemaObject https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#schemaObject + """ + multiple_of: builtins.float + maximum: builtins.float + """Maximum represents an inclusive upper limit for a numeric instance. The + value of MUST be a number, + """ + exclusive_maximum: builtins.bool + minimum: builtins.float + """minimum represents an inclusive lower limit for a numeric instance. The + value of MUST be a number, + """ + exclusive_minimum: builtins.bool + max_length: builtins.int + min_length: builtins.int + pattern: builtins.str + max_items: builtins.int + min_items: builtins.int + unique_items: builtins.bool + max_properties: builtins.int + min_properties: builtins.int + format: builtins.str + """`Format`""" + @property + def required(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + @property + def array(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """Items in 'array' must be unique.""" + + @property + def type(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___JSONSchema.JSONSchemaSimpleTypes.ValueType]: ... + @property + def enum(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """Items in `enum` must be unique https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.5.1""" + + def __init__( + self, + *, + ref: builtins.str = ..., + title: builtins.str = ..., + description: builtins.str = ..., + default: builtins.str = ..., + read_only: builtins.bool = ..., + example: builtins.str = ..., + multiple_of: builtins.float = ..., + maximum: builtins.float = ..., + exclusive_maximum: builtins.bool = ..., + minimum: builtins.float = ..., + exclusive_minimum: builtins.bool = ..., + max_length: builtins.int = ..., + min_length: builtins.int = ..., + pattern: builtins.str = ..., + max_items: builtins.int = ..., + min_items: builtins.int = ..., + unique_items: builtins.bool = ..., + max_properties: builtins.int = ..., + min_properties: builtins.int = ..., + required: collections.abc.Iterable[builtins.str] | None = ..., + array: collections.abc.Iterable[builtins.str] | None = ..., + type: collections.abc.Iterable[global___JSONSchema.JSONSchemaSimpleTypes.ValueType] | None = ..., + format: builtins.str = ..., + enum: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["array", b"array", "default", b"default", "description", b"description", "enum", b"enum", "example", b"example", "exclusive_maximum", b"exclusive_maximum", "exclusive_minimum", b"exclusive_minimum", "format", b"format", "max_items", b"max_items", "max_length", b"max_length", "max_properties", b"max_properties", "maximum", b"maximum", "min_items", b"min_items", "min_length", b"min_length", "min_properties", b"min_properties", "minimum", b"minimum", "multiple_of", b"multiple_of", "pattern", b"pattern", "read_only", b"read_only", "ref", b"ref", "required", b"required", "title", b"title", "type", b"type", "unique_items", b"unique_items"]) -> None: ... + +global___JSONSchema = JSONSchema + +@typing.final +class Tag(google.protobuf.message.Message): + """`Tag` is a representation of OpenAPI v2 specification's Tag object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#tagObject + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DESCRIPTION_FIELD_NUMBER: builtins.int + EXTERNAL_DOCS_FIELD_NUMBER: builtins.int + description: builtins.str + """A short description for the tag. GFM syntax can be used for rich text + representation. + """ + @property + def external_docs(self) -> global___ExternalDocumentation: + """Additional external documentation for this tag.""" + + def __init__( + self, + *, + description: builtins.str = ..., + external_docs: global___ExternalDocumentation | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["external_docs", b"external_docs"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["description", b"description", "external_docs", b"external_docs"]) -> None: ... + +global___Tag = Tag + +@typing.final +class SecurityDefinitions(google.protobuf.message.Message): + """`SecurityDefinitions` is a representation of OpenAPI v2 specification's + Security Definitions object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#securityDefinitionsObject + + A declaration of the security schemes available to be used in the + specification. This does not enforce the security schemes on the operations + and only serves to provide the relevant details for each scheme. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing.final + class SecurityEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___SecurityScheme: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___SecurityScheme | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + SECURITY_FIELD_NUMBER: builtins.int + @property + def security(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___SecurityScheme]: + """A single security scheme definition, mapping a "name" to the scheme it + defines. + """ + + def __init__( + self, + *, + security: collections.abc.Mapping[builtins.str, global___SecurityScheme] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["security", b"security"]) -> None: ... + +global___SecurityDefinitions = SecurityDefinitions + +@typing.final +class SecurityScheme(google.protobuf.message.Message): + """`SecurityScheme` is a representation of OpenAPI v2 specification's + Security Scheme object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#securitySchemeObject + + Allows the definition of a security scheme that can be used by the + operations. Supported schemes are basic authentication, an API key (either as + a header or as a query parameter) and OAuth2's common flows (implicit, + password, application and access code). + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _Type: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _TypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SecurityScheme._Type.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + TYPE_INVALID: SecurityScheme._Type.ValueType # 0 + TYPE_BASIC: SecurityScheme._Type.ValueType # 1 + TYPE_API_KEY: SecurityScheme._Type.ValueType # 2 + TYPE_OAUTH2: SecurityScheme._Type.ValueType # 3 + + class Type(_Type, metaclass=_TypeEnumTypeWrapper): + """The type of the security scheme. Valid values are "basic", + "apiKey" or "oauth2". + """ + + TYPE_INVALID: SecurityScheme.Type.ValueType # 0 + TYPE_BASIC: SecurityScheme.Type.ValueType # 1 + TYPE_API_KEY: SecurityScheme.Type.ValueType # 2 + TYPE_OAUTH2: SecurityScheme.Type.ValueType # 3 + + class _In: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _InEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SecurityScheme._In.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + IN_INVALID: SecurityScheme._In.ValueType # 0 + IN_QUERY: SecurityScheme._In.ValueType # 1 + IN_HEADER: SecurityScheme._In.ValueType # 2 + + class In(_In, metaclass=_InEnumTypeWrapper): + """The location of the API key. Valid values are "query" or "header".""" + + IN_INVALID: SecurityScheme.In.ValueType # 0 + IN_QUERY: SecurityScheme.In.ValueType # 1 + IN_HEADER: SecurityScheme.In.ValueType # 2 + + class _Flow: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _FlowEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SecurityScheme._Flow.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + FLOW_INVALID: SecurityScheme._Flow.ValueType # 0 + FLOW_IMPLICIT: SecurityScheme._Flow.ValueType # 1 + FLOW_PASSWORD: SecurityScheme._Flow.ValueType # 2 + FLOW_APPLICATION: SecurityScheme._Flow.ValueType # 3 + FLOW_ACCESS_CODE: SecurityScheme._Flow.ValueType # 4 + + class Flow(_Flow, metaclass=_FlowEnumTypeWrapper): + """The flow used by the OAuth2 security scheme. Valid values are + "implicit", "password", "application" or "accessCode". + """ + + FLOW_INVALID: SecurityScheme.Flow.ValueType # 0 + FLOW_IMPLICIT: SecurityScheme.Flow.ValueType # 1 + FLOW_PASSWORD: SecurityScheme.Flow.ValueType # 2 + FLOW_APPLICATION: SecurityScheme.Flow.ValueType # 3 + FLOW_ACCESS_CODE: SecurityScheme.Flow.ValueType # 4 + + @typing.final + class ExtensionsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> google.protobuf.struct_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: google.protobuf.struct_pb2.Value | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + TYPE_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + IN_FIELD_NUMBER: builtins.int + FLOW_FIELD_NUMBER: builtins.int + AUTHORIZATION_URL_FIELD_NUMBER: builtins.int + TOKEN_URL_FIELD_NUMBER: builtins.int + SCOPES_FIELD_NUMBER: builtins.int + EXTENSIONS_FIELD_NUMBER: builtins.int + type: global___SecurityScheme.Type.ValueType + """The type of the security scheme. Valid values are "basic", + "apiKey" or "oauth2". + """ + description: builtins.str + """A short description for security scheme.""" + name: builtins.str + """The name of the header or query parameter to be used. + Valid for apiKey. + """ + flow: global___SecurityScheme.Flow.ValueType + """The flow used by the OAuth2 security scheme. Valid values are + "implicit", "password", "application" or "accessCode". + Valid for oauth2. + """ + authorization_url: builtins.str + """The authorization URL to be used for this flow. This SHOULD be in + the form of a URL. + Valid for oauth2/implicit and oauth2/accessCode. + """ + token_url: builtins.str + """The token URL to be used for this flow. This SHOULD be in the + form of a URL. + Valid for oauth2/password, oauth2/application and oauth2/accessCode. + """ + @property + def scopes(self) -> global___Scopes: + """The available scopes for the OAuth2 security scheme. + Valid for oauth2. + """ + + @property + def extensions(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, google.protobuf.struct_pb2.Value]: ... + def __init__( + self, + *, + type: global___SecurityScheme.Type.ValueType = ..., + description: builtins.str = ..., + name: builtins.str = ..., + flow: global___SecurityScheme.Flow.ValueType = ..., + authorization_url: builtins.str = ..., + token_url: builtins.str = ..., + scopes: global___Scopes | None = ..., + extensions: collections.abc.Mapping[builtins.str, google.protobuf.struct_pb2.Value] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["scopes", b"scopes"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["authorization_url", b"authorization_url", "description", b"description", "extensions", b"extensions", "flow", b"flow", "in", b"in", "name", b"name", "scopes", b"scopes", "token_url", b"token_url", "type", b"type"]) -> None: ... + +global___SecurityScheme = SecurityScheme + +@typing.final +class SecurityRequirement(google.protobuf.message.Message): + """`SecurityRequirement` is a representation of OpenAPI v2 specification's + Security Requirement object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#securityRequirementObject + + Lists the required security schemes to execute this operation. The object can + have multiple security schemes declared in it which are all required (that + is, there is a logical AND between the schemes). + + The name used for each property MUST correspond to a security scheme + declared in the Security Definitions. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing.final + class SecurityRequirementValue(google.protobuf.message.Message): + """If the security scheme is of type "oauth2", then the value is a list of + scope names required for the execution. For other security scheme types, + the array MUST be empty. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SCOPE_FIELD_NUMBER: builtins.int + @property + def scope(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + def __init__( + self, + *, + scope: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["scope", b"scope"]) -> None: ... + + @typing.final + class SecurityRequirementEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___SecurityRequirement.SecurityRequirementValue: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___SecurityRequirement.SecurityRequirementValue | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + SECURITY_REQUIREMENT_FIELD_NUMBER: builtins.int + @property + def security_requirement(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___SecurityRequirement.SecurityRequirementValue]: + """Each name must correspond to a security scheme which is declared in + the Security Definitions. If the security scheme is of type "oauth2", + then the value is a list of scope names required for the execution. + For other security scheme types, the array MUST be empty. + """ + + def __init__( + self, + *, + security_requirement: collections.abc.Mapping[builtins.str, global___SecurityRequirement.SecurityRequirementValue] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["security_requirement", b"security_requirement"]) -> None: ... + +global___SecurityRequirement = SecurityRequirement + +@typing.final +class Scopes(google.protobuf.message.Message): + """`Scopes` is a representation of OpenAPI v2 specification's Scopes object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#scopesObject + + Lists the available scopes for an OAuth2 security scheme. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing.final + class ScopeEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + SCOPE_FIELD_NUMBER: builtins.int + @property + def scope(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """Maps between a name of a scope to a short description of it (as the value + of the property). + """ + + def __init__( + self, + *, + scope: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["scope", b"scope"]) -> None: ... + +global___Scopes = Scopes diff --git a/src/protoc_gen_swagger/options/openapiv2_pb2_grpc.py b/src/protoc_gen_swagger/options/openapiv2_pb2_grpc.py new file mode 100644 index 00000000..929d0253 --- /dev/null +++ b/src/protoc_gen_swagger/options/openapiv2_pb2_grpc.py @@ -0,0 +1,24 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + + +GRPC_GENERATED_VERSION = '1.73.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in protoc_gen_swagger/options/openapiv2_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) diff --git a/src/scanoss/__init__.py b/src/scanoss/__init__.py index 576ce502..7fd460f3 100644 --- a/src/scanoss/__init__.py +++ b/src/scanoss/__init__.py @@ -1,25 +1,25 @@ """ - SPDX-License-Identifier: MIT +SPDX-License-Identifier: MIT - Copyright (c) 2021, SCANOSS + Copyright (c) 2021, SCANOSS - 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: + 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 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. + 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. """ -__version__ = '0.9.0' +__version__ = '1.52.1' diff --git a/src/scanoss/api/__init__.py b/src/scanoss/api/__init__.py index d2ed05b0..c67e64da 100644 --- a/src/scanoss/api/__init__.py +++ b/src/scanoss/api/__init__.py @@ -1,23 +1,23 @@ """ - SPDX-License-Identifier: MIT +SPDX-License-Identifier: MIT - Copyright (c) 2021, SCANOSS + Copyright (c) 2021, SCANOSS - 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: + 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 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. + 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/src/scanoss/api/common/__init__.py b/src/scanoss/api/common/__init__.py index d2ed05b0..c67e64da 100644 --- a/src/scanoss/api/common/__init__.py +++ b/src/scanoss/api/common/__init__.py @@ -1,23 +1,23 @@ """ - SPDX-License-Identifier: MIT +SPDX-License-Identifier: MIT - Copyright (c) 2021, SCANOSS + Copyright (c) 2021, SCANOSS - 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: + 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 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. + 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/src/scanoss/api/common/v2/__init__.py b/src/scanoss/api/common/v2/__init__.py index d2ed05b0..c67e64da 100644 --- a/src/scanoss/api/common/v2/__init__.py +++ b/src/scanoss/api/common/v2/__init__.py @@ -1,23 +1,23 @@ """ - SPDX-License-Identifier: MIT +SPDX-License-Identifier: MIT - Copyright (c) 2021, SCANOSS + Copyright (c) 2021, SCANOSS - 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: + 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 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. + 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/src/scanoss/api/common/v2/scanoss_common_pb2.py b/src/scanoss/api/common/v2/scanoss_common_pb2.py index c787428e..3daa20b9 100644 --- a/src/scanoss/api/common/v2/scanoss_common_pb2.py +++ b/src/scanoss/api/common/v2/scanoss_common_pb2.py @@ -1,207 +1,65 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: scanoss/api/common/v2/scanoss-common.proto +# Protobuf Python Version: 6.31.0 """Generated protocol buffer code.""" -from google.protobuf.internal import enum_type_wrapper from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 0, + '', + 'scanoss/api/common/v2/scanoss-common.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='scanoss/api/common/v2/scanoss-common.proto', - package='scanoss.api.common.v2', - syntax='proto3', - serialized_options=b'Z-github.amrom.workers.dev/scanoss/papi/api/commonv2;commonv2', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n*scanoss/api/common/v2/scanoss-common.proto\x12\x15scanoss.api.common.v2\"T\n\x0eStatusResponse\x12\x31\n\x06status\x18\x01 \x01(\x0e\x32!.scanoss.api.common.v2.StatusCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x1e\n\x0b\x45\x63hoRequest\x12\x0f\n\x07message\x18\x01 \x01(\t\"\x1f\n\x0c\x45\x63hoResponse\x12\x0f\n\x07message\x18\x01 \x01(\t*`\n\nStatusCode\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0b\n\x07SUCCESS\x10\x01\x12\x1b\n\x17SUCCEEDED_WITH_WARNINGS\x10\x02\x12\x0b\n\x07WARNING\x10\x03\x12\n\n\x06\x46\x41ILED\x10\x04\x42/Z-github.amrom.workers.dev/scanoss/papi/api/commonv2;commonv2b\x06proto3' -) - -_STATUSCODE = _descriptor.EnumDescriptor( - name='StatusCode', - full_name='scanoss.api.common.v2.StatusCode', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='UNSPECIFIED', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SUCCESS', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SUCCEEDED_WITH_WARNINGS', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='WARNING', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='FAILED', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=220, - serialized_end=316, -) -_sym_db.RegisterEnumDescriptor(_STATUSCODE) - -StatusCode = enum_type_wrapper.EnumTypeWrapper(_STATUSCODE) -UNSPECIFIED = 0 -SUCCESS = 1 -SUCCEEDED_WITH_WARNINGS = 2 -WARNING = 3 -FAILED = 4 - - - -_STATUSRESPONSE = _descriptor.Descriptor( - name='StatusResponse', - full_name='scanoss.api.common.v2.StatusResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='status', full_name='scanoss.api.common.v2.StatusResponse.status', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='message', full_name='scanoss.api.common.v2.StatusResponse.message', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=69, - serialized_end=153, -) - - -_ECHOREQUEST = _descriptor.Descriptor( - name='EchoRequest', - full_name='scanoss.api.common.v2.EchoRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='message', full_name='scanoss.api.common.v2.EchoRequest.message', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=155, - serialized_end=185, -) - - -_ECHORESPONSE = _descriptor.Descriptor( - name='EchoResponse', - full_name='scanoss.api.common.v2.EchoResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='message', full_name='scanoss.api.common.v2.EchoResponse.message', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=187, - serialized_end=218, -) - -_STATUSRESPONSE.fields_by_name['status'].enum_type = _STATUSCODE -DESCRIPTOR.message_types_by_name['StatusResponse'] = _STATUSRESPONSE -DESCRIPTOR.message_types_by_name['EchoRequest'] = _ECHOREQUEST -DESCRIPTOR.message_types_by_name['EchoResponse'] = _ECHORESPONSE -DESCRIPTOR.enum_types_by_name['StatusCode'] = _STATUSCODE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -StatusResponse = _reflection.GeneratedProtocolMessageType('StatusResponse', (_message.Message,), { - 'DESCRIPTOR' : _STATUSRESPONSE, - '__module__' : 'scanoss.api.common.v2.scanoss_common_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.common.v2.StatusResponse) - }) -_sym_db.RegisterMessage(StatusResponse) - -EchoRequest = _reflection.GeneratedProtocolMessageType('EchoRequest', (_message.Message,), { - 'DESCRIPTOR' : _ECHOREQUEST, - '__module__' : 'scanoss.api.common.v2.scanoss_common_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.common.v2.EchoRequest) - }) -_sym_db.RegisterMessage(EchoRequest) - -EchoResponse = _reflection.GeneratedProtocolMessageType('EchoResponse', (_message.Message,), { - 'DESCRIPTOR' : _ECHORESPONSE, - '__module__' : 'scanoss.api.common.v2.scanoss_common_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.common.v2.EchoResponse) - }) -_sym_db.RegisterMessage(EchoResponse) - - -DESCRIPTOR._options = None +from protoc_gen_openapiv2.options import annotations_pb2 as protoc__gen__openapiv2_dot_options_dot_annotations__pb2 +from google.api import field_behavior_pb2 as google_dot_api_dot_field__behavior__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n*scanoss/api/common/v2/scanoss-common.proto\x12\x15scanoss.api.common.v2\x1a.protoc-gen-openapiv2/options/annotations.proto\x1a\x1fgoogle/api/field_behavior.proto\"T\n\x0eStatusResponse\x12\x31\n\x06status\x18\x01 \x01(\x0e\x32!.scanoss.api.common.v2.StatusCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x1e\n\x0b\x45\x63hoRequest\x12\x0f\n\x07message\x18\x01 \x01(\t\"\x1f\n\x0c\x45\x63hoResponse\x12\x0f\n\x07message\x18\x01 \x01(\t\"k\n\x10\x43omponentRequest\x12\x11\n\x04purl\x18\x01 \x01(\tB\x03\xe0\x41\x02\x12\x13\n\x0brequirement\x18\x02 \x01(\t:/\x92\x41,2*{\"purl\":\"pkg:github/scanoss/engine@1.0.0\"}\"\xc8\x01\n\x11\x43omponentsRequest\x12@\n\ncomponents\x18\x01 \x03(\x0b\x32\'.scanoss.api.common.v2.ComponentRequestB\x03\xe0\x41\x02:q\x92\x41n2l{\"components\":[{\"purl\":\"pkg:github/scanoss/engine@1.0.0\"},{\"purl\":\"pkg:github/scanoss/scanoss.py@v1.30.0\"}]}\"v\n\x0bPurlRequest\x12\x37\n\x05purls\x18\x01 \x03(\x0b\x32(.scanoss.api.common.v2.PurlRequest.Purls\x1a*\n\x05Purls\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x13\n\x0brequirement\x18\x02 \x01(\t:\x02\x18\x01\")\n\x04Purl\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x13\n\x0brequirement\x18\x02 \x01(\t*`\n\nStatusCode\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0b\n\x07SUCCESS\x10\x01\x12\x1b\n\x17SUCCEEDED_WITH_WARNINGS\x10\x02\x12\x0b\n\x07WARNING\x10\x03\x12\n\n\x06\x46\x41ILED\x10\x04\x42/Z-github.amrom.workers.dev/scanoss/papi/api/commonv2;commonv2b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'scanoss.api.common.v2.scanoss_common_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z-github.amrom.workers.dev/scanoss/papi/api/commonv2;commonv2' + _globals['_COMPONENTREQUEST'].fields_by_name['purl']._loaded_options = None + _globals['_COMPONENTREQUEST'].fields_by_name['purl']._serialized_options = b'\340A\002' + _globals['_COMPONENTREQUEST']._loaded_options = None + _globals['_COMPONENTREQUEST']._serialized_options = b'\222A,2*{\"purl\":\"pkg:github/scanoss/engine@1.0.0\"}' + _globals['_COMPONENTSREQUEST'].fields_by_name['components']._loaded_options = None + _globals['_COMPONENTSREQUEST'].fields_by_name['components']._serialized_options = b'\340A\002' + _globals['_COMPONENTSREQUEST']._loaded_options = None + _globals['_COMPONENTSREQUEST']._serialized_options = b'\222An2l{\"components\":[{\"purl\":\"pkg:github/scanoss/engine@1.0.0\"},{\"purl\":\"pkg:github/scanoss/scanoss.py@v1.30.0\"}]}' + _globals['_PURLREQUEST']._loaded_options = None + _globals['_PURLREQUEST']._serialized_options = b'\030\001' + _globals['_STATUSCODE']._serialized_start=776 + _globals['_STATUSCODE']._serialized_end=872 + _globals['_STATUSRESPONSE']._serialized_start=150 + _globals['_STATUSRESPONSE']._serialized_end=234 + _globals['_ECHOREQUEST']._serialized_start=236 + _globals['_ECHOREQUEST']._serialized_end=266 + _globals['_ECHORESPONSE']._serialized_start=268 + _globals['_ECHORESPONSE']._serialized_end=299 + _globals['_COMPONENTREQUEST']._serialized_start=301 + _globals['_COMPONENTREQUEST']._serialized_end=408 + _globals['_COMPONENTSREQUEST']._serialized_start=411 + _globals['_COMPONENTSREQUEST']._serialized_end=611 + _globals['_PURLREQUEST']._serialized_start=613 + _globals['_PURLREQUEST']._serialized_end=731 + _globals['_PURLREQUEST_PURLS']._serialized_start=685 + _globals['_PURLREQUEST_PURLS']._serialized_end=727 + _globals['_PURL']._serialized_start=733 + _globals['_PURL']._serialized_end=774 # @@protoc_insertion_point(module_scope) diff --git a/src/scanoss/api/common/v2/scanoss_common_pb2_grpc.py b/src/scanoss/api/common/v2/scanoss_common_pb2_grpc.py index 2daafffe..13118697 100644 --- a/src/scanoss/api/common/v2/scanoss_common_pb2_grpc.py +++ b/src/scanoss/api/common/v2/scanoss_common_pb2_grpc.py @@ -1,4 +1,29 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" +import warnings + import grpc +import warnings + +GRPC_GENERATED_VERSION = '1.73.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +GRPC_GENERATED_VERSION = '1.73.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in scanoss/api/common/v2/scanoss_common_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) diff --git a/src/scanoss/api/components/__init__.py b/src/scanoss/api/components/__init__.py index d2ed05b0..c67e64da 100644 --- a/src/scanoss/api/components/__init__.py +++ b/src/scanoss/api/components/__init__.py @@ -1,23 +1,23 @@ """ - SPDX-License-Identifier: MIT +SPDX-License-Identifier: MIT - Copyright (c) 2021, SCANOSS + Copyright (c) 2021, SCANOSS - 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: + 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 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. + 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/src/scanoss/api/components/v2/__init__.py b/src/scanoss/api/components/v2/__init__.py index d2ed05b0..c67e64da 100644 --- a/src/scanoss/api/components/v2/__init__.py +++ b/src/scanoss/api/components/v2/__init__.py @@ -1,23 +1,23 @@ """ - SPDX-License-Identifier: MIT +SPDX-License-Identifier: MIT - Copyright (c) 2021, SCANOSS + Copyright (c) 2021, SCANOSS - 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: + 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 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. + 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/src/scanoss/api/components/v2/scanoss_components_pb2.py b/src/scanoss/api/components/v2/scanoss_components_pb2.py index b4b09a6b..fb2a523a 100644 --- a/src/scanoss/api/components/v2/scanoss_components_pb2.py +++ b/src/scanoss/api/components/v2/scanoss_components_pb2.py @@ -1,524 +1,86 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: scanoss/api/components/v2/scanoss-components.proto +# Protobuf Python Version: 6.31.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 0, + '', + 'scanoss/api/components/v2/scanoss-components.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='scanoss/api/components/v2/scanoss-components.proto', - package='scanoss.api.components.v2', - syntax='proto3', - serialized_options=b'Z5github.amrom.workers.dev/scanoss/papi/api/componentsv2;componentsv2', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n2scanoss/api/components/v2/scanoss-components.proto\x12\x19scanoss.api.components.v2\x1a*scanoss/api/common/v2/scanoss-common.proto\"v\n\x11\x43ompSearchRequest\x12\x0e\n\x06search\x18\x01 \x01(\t\x12\x0e\n\x06vendor\x18\x02 \x01(\t\x12\x11\n\tcomponent\x18\x03 \x01(\t\x12\x0f\n\x07package\x18\x04 \x01(\t\x12\r\n\x05limit\x18\x06 \x01(\x05\x12\x0e\n\x06offset\x18\x07 \x01(\x05\"\xd3\x01\n\x12\x43ompSearchResponse\x12K\n\ncomponents\x18\x01 \x03(\x0b\x32\x37.scanoss.api.components.v2.CompSearchResponse.Component\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1a\x39\n\tComponent\x12\x11\n\tcomponent\x18\x01 \x01(\t\x12\x0c\n\x04purl\x18\x02 \x01(\t\x12\x0b\n\x03url\x18\x03 \x01(\t\"1\n\x12\x43ompVersionRequest\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x05\"\xd6\x03\n\x13\x43ompVersionResponse\x12K\n\tcomponent\x18\x01 \x01(\x0b\x32\x38.scanoss.api.components.v2.CompVersionResponse.Component\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1aO\n\x07License\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07spdx_id\x18\x02 \x01(\t\x12\x18\n\x10is_spdx_approved\x18\x03 \x01(\x08\x12\x0b\n\x03url\x18\x04 \x01(\t\x1a\x64\n\x07Version\x12\x0f\n\x07version\x18\x01 \x01(\t\x12H\n\x08licenses\x18\x04 \x03(\x0b\x32\x36.scanoss.api.components.v2.CompVersionResponse.License\x1a\x83\x01\n\tComponent\x12\x11\n\tcomponent\x18\x01 \x01(\t\x12\x0c\n\x04purl\x18\x02 \x01(\t\x12\x0b\n\x03url\x18\x03 \x01(\t\x12H\n\x08versions\x18\x04 \x03(\x0b\x32\x36.scanoss.api.components.v2.CompVersionResponse.Version2\xc5\x02\n\nComponents\x12O\n\x04\x45\x63ho\x12\".scanoss.api.common.v2.EchoRequest\x1a#.scanoss.api.common.v2.EchoResponse\x12o\n\x10SearchComponents\x12,.scanoss.api.components.v2.CompSearchRequest\x1a-.scanoss.api.components.v2.CompSearchResponse\x12u\n\x14GetComponentVersions\x12-.scanoss.api.components.v2.CompVersionRequest\x1a..scanoss.api.components.v2.CompVersionResponseB7Z5github.amrom.workers.dev/scanoss/papi/api/componentsv2;componentsv2b\x06proto3' - , - dependencies=[scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.DESCRIPTOR,]) - - - - -_COMPSEARCHREQUEST = _descriptor.Descriptor( - name='CompSearchRequest', - full_name='scanoss.api.components.v2.CompSearchRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='search', full_name='scanoss.api.components.v2.CompSearchRequest.search', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='vendor', full_name='scanoss.api.components.v2.CompSearchRequest.vendor', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='component', full_name='scanoss.api.components.v2.CompSearchRequest.component', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='package', full_name='scanoss.api.components.v2.CompSearchRequest.package', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='limit', full_name='scanoss.api.components.v2.CompSearchRequest.limit', index=4, - number=6, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='offset', full_name='scanoss.api.components.v2.CompSearchRequest.offset', index=5, - number=7, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=125, - serialized_end=243, -) - - -_COMPSEARCHRESPONSE_COMPONENT = _descriptor.Descriptor( - name='Component', - full_name='scanoss.api.components.v2.CompSearchResponse.Component', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='component', full_name='scanoss.api.components.v2.CompSearchResponse.Component.component', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='purl', full_name='scanoss.api.components.v2.CompSearchResponse.Component.purl', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='url', full_name='scanoss.api.components.v2.CompSearchResponse.Component.url', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=400, - serialized_end=457, -) - -_COMPSEARCHRESPONSE = _descriptor.Descriptor( - name='CompSearchResponse', - full_name='scanoss.api.components.v2.CompSearchResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='components', full_name='scanoss.api.components.v2.CompSearchResponse.components', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='status', full_name='scanoss.api.components.v2.CompSearchResponse.status', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_COMPSEARCHRESPONSE_COMPONENT, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=246, - serialized_end=457, -) - - -_COMPVERSIONREQUEST = _descriptor.Descriptor( - name='CompVersionRequest', - full_name='scanoss.api.components.v2.CompVersionRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='purl', full_name='scanoss.api.components.v2.CompVersionRequest.purl', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='limit', full_name='scanoss.api.components.v2.CompVersionRequest.limit', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=459, - serialized_end=508, -) - - -_COMPVERSIONRESPONSE_LICENSE = _descriptor.Descriptor( - name='License', - full_name='scanoss.api.components.v2.CompVersionResponse.License', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='scanoss.api.components.v2.CompVersionResponse.License.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='spdx_id', full_name='scanoss.api.components.v2.CompVersionResponse.License.spdx_id', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='is_spdx_approved', full_name='scanoss.api.components.v2.CompVersionResponse.License.is_spdx_approved', index=2, - number=3, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='url', full_name='scanoss.api.components.v2.CompVersionResponse.License.url', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=666, - serialized_end=745, -) - -_COMPVERSIONRESPONSE_VERSION = _descriptor.Descriptor( - name='Version', - full_name='scanoss.api.components.v2.CompVersionResponse.Version', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='version', full_name='scanoss.api.components.v2.CompVersionResponse.Version.version', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='licenses', full_name='scanoss.api.components.v2.CompVersionResponse.Version.licenses', index=1, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=747, - serialized_end=847, -) - -_COMPVERSIONRESPONSE_COMPONENT = _descriptor.Descriptor( - name='Component', - full_name='scanoss.api.components.v2.CompVersionResponse.Component', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='component', full_name='scanoss.api.components.v2.CompVersionResponse.Component.component', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='purl', full_name='scanoss.api.components.v2.CompVersionResponse.Component.purl', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='url', full_name='scanoss.api.components.v2.CompVersionResponse.Component.url', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='versions', full_name='scanoss.api.components.v2.CompVersionResponse.Component.versions', index=3, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=850, - serialized_end=981, -) - -_COMPVERSIONRESPONSE = _descriptor.Descriptor( - name='CompVersionResponse', - full_name='scanoss.api.components.v2.CompVersionResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='component', full_name='scanoss.api.components.v2.CompVersionResponse.component', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='status', full_name='scanoss.api.components.v2.CompVersionResponse.status', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_COMPVERSIONRESPONSE_LICENSE, _COMPVERSIONRESPONSE_VERSION, _COMPVERSIONRESPONSE_COMPONENT, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=511, - serialized_end=981, -) - -_COMPSEARCHRESPONSE_COMPONENT.containing_type = _COMPSEARCHRESPONSE -_COMPSEARCHRESPONSE.fields_by_name['components'].message_type = _COMPSEARCHRESPONSE_COMPONENT -_COMPSEARCHRESPONSE.fields_by_name['status'].message_type = scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2._STATUSRESPONSE -_COMPVERSIONRESPONSE_LICENSE.containing_type = _COMPVERSIONRESPONSE -_COMPVERSIONRESPONSE_VERSION.fields_by_name['licenses'].message_type = _COMPVERSIONRESPONSE_LICENSE -_COMPVERSIONRESPONSE_VERSION.containing_type = _COMPVERSIONRESPONSE -_COMPVERSIONRESPONSE_COMPONENT.fields_by_name['versions'].message_type = _COMPVERSIONRESPONSE_VERSION -_COMPVERSIONRESPONSE_COMPONENT.containing_type = _COMPVERSIONRESPONSE -_COMPVERSIONRESPONSE.fields_by_name['component'].message_type = _COMPVERSIONRESPONSE_COMPONENT -_COMPVERSIONRESPONSE.fields_by_name['status'].message_type = scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2._STATUSRESPONSE -DESCRIPTOR.message_types_by_name['CompSearchRequest'] = _COMPSEARCHREQUEST -DESCRIPTOR.message_types_by_name['CompSearchResponse'] = _COMPSEARCHRESPONSE -DESCRIPTOR.message_types_by_name['CompVersionRequest'] = _COMPVERSIONREQUEST -DESCRIPTOR.message_types_by_name['CompVersionResponse'] = _COMPVERSIONRESPONSE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -CompSearchRequest = _reflection.GeneratedProtocolMessageType('CompSearchRequest', (_message.Message,), { - 'DESCRIPTOR' : _COMPSEARCHREQUEST, - '__module__' : 'scanoss.api.components.v2.scanoss_components_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.components.v2.CompSearchRequest) - }) -_sym_db.RegisterMessage(CompSearchRequest) - -CompSearchResponse = _reflection.GeneratedProtocolMessageType('CompSearchResponse', (_message.Message,), { - - 'Component' : _reflection.GeneratedProtocolMessageType('Component', (_message.Message,), { - 'DESCRIPTOR' : _COMPSEARCHRESPONSE_COMPONENT, - '__module__' : 'scanoss.api.components.v2.scanoss_components_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.components.v2.CompSearchResponse.Component) - }) - , - 'DESCRIPTOR' : _COMPSEARCHRESPONSE, - '__module__' : 'scanoss.api.components.v2.scanoss_components_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.components.v2.CompSearchResponse) - }) -_sym_db.RegisterMessage(CompSearchResponse) -_sym_db.RegisterMessage(CompSearchResponse.Component) - -CompVersionRequest = _reflection.GeneratedProtocolMessageType('CompVersionRequest', (_message.Message,), { - 'DESCRIPTOR' : _COMPVERSIONREQUEST, - '__module__' : 'scanoss.api.components.v2.scanoss_components_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.components.v2.CompVersionRequest) - }) -_sym_db.RegisterMessage(CompVersionRequest) - -CompVersionResponse = _reflection.GeneratedProtocolMessageType('CompVersionResponse', (_message.Message,), { - - 'License' : _reflection.GeneratedProtocolMessageType('License', (_message.Message,), { - 'DESCRIPTOR' : _COMPVERSIONRESPONSE_LICENSE, - '__module__' : 'scanoss.api.components.v2.scanoss_components_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.components.v2.CompVersionResponse.License) - }) - , - - 'Version' : _reflection.GeneratedProtocolMessageType('Version', (_message.Message,), { - 'DESCRIPTOR' : _COMPVERSIONRESPONSE_VERSION, - '__module__' : 'scanoss.api.components.v2.scanoss_components_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.components.v2.CompVersionResponse.Version) - }) - , - - 'Component' : _reflection.GeneratedProtocolMessageType('Component', (_message.Message,), { - 'DESCRIPTOR' : _COMPVERSIONRESPONSE_COMPONENT, - '__module__' : 'scanoss.api.components.v2.scanoss_components_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.components.v2.CompVersionResponse.Component) - }) - , - 'DESCRIPTOR' : _COMPVERSIONRESPONSE, - '__module__' : 'scanoss.api.components.v2.scanoss_components_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.components.v2.CompVersionResponse) - }) -_sym_db.RegisterMessage(CompVersionResponse) -_sym_db.RegisterMessage(CompVersionResponse.License) -_sym_db.RegisterMessage(CompVersionResponse.Version) -_sym_db.RegisterMessage(CompVersionResponse.Component) - - -DESCRIPTOR._options = None - -_COMPONENTS = _descriptor.ServiceDescriptor( - name='Components', - full_name='scanoss.api.components.v2.Components', - file=DESCRIPTOR, - index=0, - serialized_options=None, - create_key=_descriptor._internal_create_key, - serialized_start=984, - serialized_end=1309, - methods=[ - _descriptor.MethodDescriptor( - name='Echo', - full_name='scanoss.api.components.v2.Components.Echo', - index=0, - containing_service=None, - input_type=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2._ECHOREQUEST, - output_type=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2._ECHORESPONSE, - serialized_options=None, - create_key=_descriptor._internal_create_key, - ), - _descriptor.MethodDescriptor( - name='SearchComponents', - full_name='scanoss.api.components.v2.Components.SearchComponents', - index=1, - containing_service=None, - input_type=_COMPSEARCHREQUEST, - output_type=_COMPSEARCHRESPONSE, - serialized_options=None, - create_key=_descriptor._internal_create_key, - ), - _descriptor.MethodDescriptor( - name='GetComponentVersions', - full_name='scanoss.api.components.v2.Components.GetComponentVersions', - index=2, - containing_service=None, - input_type=_COMPVERSIONREQUEST, - output_type=_COMPVERSIONRESPONSE, - serialized_options=None, - create_key=_descriptor._internal_create_key, - ), -]) -_sym_db.RegisterServiceDescriptor(_COMPONENTS) - -DESCRIPTOR.services_by_name['Components'] = _COMPONENTS - +from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 +from protoc_gen_openapiv2.options import annotations_pb2 as protoc__gen__openapiv2_dot_options_dot_annotations__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n2scanoss/api/components/v2/scanoss-components.proto\x12\x19scanoss.api.components.v2\x1a*scanoss/api/common/v2/scanoss-common.proto\x1a\x1cgoogle/api/annotations.proto\x1a.protoc-gen-openapiv2/options/annotations.proto\"\xae\x01\n\x11\x43ompSearchRequest\x12\x0e\n\x06search\x18\x01 \x01(\t\x12\x0e\n\x06vendor\x18\x02 \x01(\t\x12\x11\n\tcomponent\x18\x03 \x01(\t\x12\x0f\n\x07package\x18\x04 \x01(\t\x12\r\n\x05limit\x18\x06 \x01(\x05\x12\x0e\n\x06offset\x18\x07 \x01(\x05:6\x92\x41\x33\n1J/{\"search\": \"scanoss\", \"limit\": 10, \"offset\": 0}\"\xfe\x01\n\rCompStatistic\x12.\n\x12total_source_files\x18\x01 \x01(\x05R\x12total_source_files\x12 \n\x0btotal_lines\x18\x02 \x01(\x05R\x0btotal_lines\x12,\n\x11total_blank_lines\x18\x03 \x01(\x05R\x11total_blank_lines\x12\x44\n\tlanguages\x18\x04 \x03(\x0b\x32\x31.scanoss.api.components.v2.CompStatistic.Language\x1a\'\n\x08Language\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05\x66iles\x18\x02 \x01(\x05\"\xb5\x05\n\x1b\x43omponentsStatisticResponse\x12~\n\x14\x63omponent_statistics\x18\x01 \x03(\x0b\x32J.scanoss.api.components.v2.ComponentsStatisticResponse.ComponentStatisticsR\x14\x63omponent_statistics\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1ar\n\x13\x43omponentStatistics\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12<\n\nstatistics\x18\x03 \x01(\x0b\x32(.scanoss.api.components.v2.CompStatistic:\xea\x02\x92\x41\xe6\x02\n\xe3\x02J\xe0\x02{\"component_statistics\": [{\"purl\": \"pkg:github/scanoss/engine@5.0.0\", \"version\": \"5.0.0\", \"statistics\": {\"total_source_files\": 156, \"total_lines\": 25430, \"total_blank_lines\": 3420, \"languages\": [{\"name\": \"C\", \"files\": 89}, {\"name\": \"C Header\", \"files\": 45}]}}], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Component statistics successfully retrieved\"}}\"\xdb\x04\n\x12\x43ompSearchResponse\x12K\n\ncomponents\x18\x01 \x03(\x0b\x32\x37.scanoss.api.components.v2.CompSearchResponse.Component\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1aK\n\tComponent\x12\x15\n\tcomponent\x18\x01 \x01(\tB\x02\x18\x01\x12\x0c\n\x04purl\x18\x02 \x01(\t\x12\x0b\n\x03url\x18\x03 \x01(\t\x12\x0c\n\x04name\x18\x04 \x01(\t:\xf3\x02\x92\x41\xef\x02\n\xec\x02J\xe9\x02{\"components\": [{\"name\": \"scanoss-py\", \"purl\": \"pkg:github/scanoss/scanoss.py\", \"url\": \"https://github.com/scanoss/scanoss.py\", \"component\": \"scanoss-py\"}, {\"name\": \"engine\", \"purl\": \"pkg:github/scanoss/engine\", \"url\": \"https://github.com/scanoss/engine\", \"component\": \"engine\"}], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Components successfully retrieved\"}}\"l\n\x12\x43ompVersionRequest\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x05:9\x92\x41\x36\n4J2{\"purl\": \"pkg:github/scanoss/engine\", \"limit\": 20}\"\xe0\x07\n\x13\x43ompVersionResponse\x12K\n\tcomponent\x18\x01 \x01(\x0b\x32\x38.scanoss.api.components.v2.CompVersionResponse.Component\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1aj\n\x07License\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x07spdx_id\x18\x02 \x01(\tR\x07spdx_id\x12*\n\x10is_spdx_approved\x18\x03 \x01(\x08R\x10is_spdx_approved\x12\x0b\n\x03url\x18\x04 \x01(\t\x1ar\n\x07Version\x12\x0f\n\x07version\x18\x01 \x01(\t\x12H\n\x08licenses\x18\x04 \x03(\x0b\x32\x36.scanoss.api.components.v2.CompVersionResponse.License\x12\x0c\n\x04\x64\x61te\x18\x05 \x01(\t\x1a\x95\x01\n\tComponent\x12\x15\n\tcomponent\x18\x01 \x01(\tB\x02\x18\x01\x12\x0c\n\x04purl\x18\x02 \x01(\t\x12\x0b\n\x03url\x18\x03 \x01(\t\x12H\n\x08versions\x18\x04 \x03(\x0b\x32\x36.scanoss.api.components.v2.CompVersionResponse.Version\x12\x0c\n\x04name\x18\x05 \x01(\t:\xcc\x03\x92\x41\xc8\x03\n\xc5\x03J\xc2\x03{\"component\": {\"name\": \"engine\", \"purl\": \"pkg:github/scanoss/engine\", \"url\": \"https://github.com/scanoss/engine\", \"versions\": [{\"version\": \"5.0.0\", \"licenses\": [{\"name\": \"GNU General Public License v2.0\", \"spdx_id\": \"GPL-2.0\", \"is_spdx_approved\": true, \"url\": \"https://spdx.org/licenses/GPL-2.0.html\"}], \"date\": \"2024-01-15T10:30:00Z\"}], \"component\": \"engine\"}, \"status\": {\"status\": \"SUCCESS\", \"message\": \"Component versions successfully retrieved\"}}2\xca\x04\n\nComponents\x12o\n\x04\x45\x63ho\x12\".scanoss.api.common.v2.EchoRequest\x1a#.scanoss.api.common.v2.EchoResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\"\x13/v2/components/echo:\x01*\x12\x8e\x01\n\x10SearchComponents\x12,.scanoss.api.components.v2.CompSearchRequest\x1a-.scanoss.api.components.v2.CompSearchResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\x12\x15/v2/components/search\x12\x96\x01\n\x14GetComponentVersions\x12-.scanoss.api.components.v2.CompVersionRequest\x1a..scanoss.api.components.v2.CompVersionResponse\"\x1f\x82\xd3\xe4\x93\x02\x19\x12\x17/v2/components/versions\x12\xa0\x01\n\x16GetComponentStatistics\x12(.scanoss.api.common.v2.ComponentsRequest\x1a\x36.scanoss.api.components.v2.ComponentsStatisticResponse\"$\x82\xd3\xe4\x93\x02\x1e\"\x19/v2/components/statistics:\x01*B\x8b\x03Z5github.amrom.workers.dev/scanoss/papi/api/componentsv2;componentsv2\x92\x41\xd0\x02\x12\x9d\x01\n\x1aSCANOSS Components Service\x12(Provides component intelligence services\"P\n\x12scanoss-components\x12%https://github.com/scanoss/components\x1a\x13support@scanoss.com2\x03\x32.0\x1a\x0f\x61pi.scanoss.com*\x02\x02\x01\x32\x10\x61pplication/json:\x10\x61pplication/jsonR;\n\x03\x34\x30\x34\x12\x34\n*Returned when the resource does not exist.\x12\x06\n\x04\x9a\x02\x01\x07Z8\n6\n\x07\x61pi_key\x12+\x08\x02\x12\x1a\x41PI key for authentication\x1a\tx-api-key \x02\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'scanoss.api.components.v2.scanoss_components_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z5github.amrom.workers.dev/scanoss/papi/api/componentsv2;componentsv2\222A\320\002\022\235\001\n\032SCANOSS Components Service\022(Provides component intelligence services\"P\n\022scanoss-components\022%https://github.com/scanoss/components\032\023support@scanoss.com2\0032.0\032\017api.scanoss.com*\002\002\0012\020application/json:\020application/jsonR;\n\003404\0224\n*Returned when the resource does not exist.\022\006\n\004\232\002\001\007Z8\n6\n\007api_key\022+\010\002\022\032API key for authentication\032\tx-api-key \002' + _globals['_COMPSEARCHREQUEST']._loaded_options = None + _globals['_COMPSEARCHREQUEST']._serialized_options = b'\222A3\n1J/{\"search\": \"scanoss\", \"limit\": 10, \"offset\": 0}' + _globals['_COMPONENTSSTATISTICRESPONSE']._loaded_options = None + _globals['_COMPONENTSSTATISTICRESPONSE']._serialized_options = b'\222A\346\002\n\343\002J\340\002{\"component_statistics\": [{\"purl\": \"pkg:github/scanoss/engine@5.0.0\", \"version\": \"5.0.0\", \"statistics\": {\"total_source_files\": 156, \"total_lines\": 25430, \"total_blank_lines\": 3420, \"languages\": [{\"name\": \"C\", \"files\": 89}, {\"name\": \"C Header\", \"files\": 45}]}}], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Component statistics successfully retrieved\"}}' + _globals['_COMPSEARCHRESPONSE_COMPONENT'].fields_by_name['component']._loaded_options = None + _globals['_COMPSEARCHRESPONSE_COMPONENT'].fields_by_name['component']._serialized_options = b'\030\001' + _globals['_COMPSEARCHRESPONSE']._loaded_options = None + _globals['_COMPSEARCHRESPONSE']._serialized_options = b'\222A\357\002\n\354\002J\351\002{\"components\": [{\"name\": \"scanoss-py\", \"purl\": \"pkg:github/scanoss/scanoss.py\", \"url\": \"https://github.com/scanoss/scanoss.py\", \"component\": \"scanoss-py\"}, {\"name\": \"engine\", \"purl\": \"pkg:github/scanoss/engine\", \"url\": \"https://github.com/scanoss/engine\", \"component\": \"engine\"}], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Components successfully retrieved\"}}' + _globals['_COMPVERSIONREQUEST']._loaded_options = None + _globals['_COMPVERSIONREQUEST']._serialized_options = b'\222A6\n4J2{\"purl\": \"pkg:github/scanoss/engine\", \"limit\": 20}' + _globals['_COMPVERSIONRESPONSE_COMPONENT'].fields_by_name['component']._loaded_options = None + _globals['_COMPVERSIONRESPONSE_COMPONENT'].fields_by_name['component']._serialized_options = b'\030\001' + _globals['_COMPVERSIONRESPONSE']._loaded_options = None + _globals['_COMPVERSIONRESPONSE']._serialized_options = b'\222A\310\003\n\305\003J\302\003{\"component\": {\"name\": \"engine\", \"purl\": \"pkg:github/scanoss/engine\", \"url\": \"https://github.com/scanoss/engine\", \"versions\": [{\"version\": \"5.0.0\", \"licenses\": [{\"name\": \"GNU General Public License v2.0\", \"spdx_id\": \"GPL-2.0\", \"is_spdx_approved\": true, \"url\": \"https://spdx.org/licenses/GPL-2.0.html\"}], \"date\": \"2024-01-15T10:30:00Z\"}], \"component\": \"engine\"}, \"status\": {\"status\": \"SUCCESS\", \"message\": \"Component versions successfully retrieved\"}}' + _globals['_COMPONENTS'].methods_by_name['Echo']._loaded_options = None + _globals['_COMPONENTS'].methods_by_name['Echo']._serialized_options = b'\202\323\344\223\002\030\"\023/v2/components/echo:\001*' + _globals['_COMPONENTS'].methods_by_name['SearchComponents']._loaded_options = None + _globals['_COMPONENTS'].methods_by_name['SearchComponents']._serialized_options = b'\202\323\344\223\002\027\022\025/v2/components/search' + _globals['_COMPONENTS'].methods_by_name['GetComponentVersions']._loaded_options = None + _globals['_COMPONENTS'].methods_by_name['GetComponentVersions']._serialized_options = b'\202\323\344\223\002\031\022\027/v2/components/versions' + _globals['_COMPONENTS'].methods_by_name['GetComponentStatistics']._loaded_options = None + _globals['_COMPONENTS'].methods_by_name['GetComponentStatistics']._serialized_options = b'\202\323\344\223\002\036\"\031/v2/components/statistics:\001*' + _globals['_COMPSEARCHREQUEST']._serialized_start=204 + _globals['_COMPSEARCHREQUEST']._serialized_end=378 + _globals['_COMPSTATISTIC']._serialized_start=381 + _globals['_COMPSTATISTIC']._serialized_end=635 + _globals['_COMPSTATISTIC_LANGUAGE']._serialized_start=596 + _globals['_COMPSTATISTIC_LANGUAGE']._serialized_end=635 + _globals['_COMPONENTSSTATISTICRESPONSE']._serialized_start=638 + _globals['_COMPONENTSSTATISTICRESPONSE']._serialized_end=1331 + _globals['_COMPONENTSSTATISTICRESPONSE_COMPONENTSTATISTICS']._serialized_start=852 + _globals['_COMPONENTSSTATISTICRESPONSE_COMPONENTSTATISTICS']._serialized_end=966 + _globals['_COMPSEARCHRESPONSE']._serialized_start=1334 + _globals['_COMPSEARCHRESPONSE']._serialized_end=1937 + _globals['_COMPSEARCHRESPONSE_COMPONENT']._serialized_start=1488 + _globals['_COMPSEARCHRESPONSE_COMPONENT']._serialized_end=1563 + _globals['_COMPVERSIONREQUEST']._serialized_start=1939 + _globals['_COMPVERSIONREQUEST']._serialized_end=2047 + _globals['_COMPVERSIONRESPONSE']._serialized_start=2050 + _globals['_COMPVERSIONRESPONSE']._serialized_end=3042 + _globals['_COMPVERSIONRESPONSE_LICENSE']._serialized_start=2205 + _globals['_COMPVERSIONRESPONSE_LICENSE']._serialized_end=2311 + _globals['_COMPVERSIONRESPONSE_VERSION']._serialized_start=2313 + _globals['_COMPVERSIONRESPONSE_VERSION']._serialized_end=2427 + _globals['_COMPVERSIONRESPONSE_COMPONENT']._serialized_start=2430 + _globals['_COMPVERSIONRESPONSE_COMPONENT']._serialized_end=2579 + _globals['_COMPONENTS']._serialized_start=3045 + _globals['_COMPONENTS']._serialized_end=3631 # @@protoc_insertion_point(module_scope) diff --git a/src/scanoss/api/components/v2/scanoss_components_pb2_grpc.py b/src/scanoss/api/components/v2/scanoss_components_pb2_grpc.py index c2f1e84d..47490fbd 100644 --- a/src/scanoss/api/components/v2/scanoss_components_pb2_grpc.py +++ b/src/scanoss/api/components/v2/scanoss_components_pb2_grpc.py @@ -1,10 +1,30 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc +import warnings from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 from scanoss.api.components.v2 import scanoss_components_pb2 as scanoss_dot_api_dot_components_dot_v2_dot_scanoss__components__pb2 +GRPC_GENERATED_VERSION = '1.73.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in scanoss/api/components/v2/scanoss_components_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + class ComponentsStub(object): """ @@ -21,17 +41,22 @@ def __init__(self, channel): '/scanoss.api.components.v2.Components/Echo', request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, response_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, - ) + _registered_method=True) self.SearchComponents = channel.unary_unary( '/scanoss.api.components.v2.Components/SearchComponents', request_serializer=scanoss_dot_api_dot_components_dot_v2_dot_scanoss__components__pb2.CompSearchRequest.SerializeToString, response_deserializer=scanoss_dot_api_dot_components_dot_v2_dot_scanoss__components__pb2.CompSearchResponse.FromString, - ) + _registered_method=True) self.GetComponentVersions = channel.unary_unary( '/scanoss.api.components.v2.Components/GetComponentVersions', request_serializer=scanoss_dot_api_dot_components_dot_v2_dot_scanoss__components__pb2.CompVersionRequest.SerializeToString, response_deserializer=scanoss_dot_api_dot_components_dot_v2_dot_scanoss__components__pb2.CompVersionResponse.FromString, - ) + _registered_method=True) + self.GetComponentStatistics = channel.unary_unary( + '/scanoss.api.components.v2.Components/GetComponentStatistics', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_components_dot_v2_dot_scanoss__components__pb2.ComponentsStatisticResponse.FromString, + _registered_method=True) class ComponentsServicer(object): @@ -60,6 +85,13 @@ def GetComponentVersions(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def GetComponentStatistics(self, request, context): + """Get the statistics for the specified components + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_ComponentsServicer_to_server(servicer, server): rpc_method_handlers = { @@ -78,10 +110,16 @@ def add_ComponentsServicer_to_server(servicer, server): request_deserializer=scanoss_dot_api_dot_components_dot_v2_dot_scanoss__components__pb2.CompVersionRequest.FromString, response_serializer=scanoss_dot_api_dot_components_dot_v2_dot_scanoss__components__pb2.CompVersionResponse.SerializeToString, ), + 'GetComponentStatistics': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentStatistics, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.FromString, + response_serializer=scanoss_dot_api_dot_components_dot_v2_dot_scanoss__components__pb2.ComponentsStatisticResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'scanoss.api.components.v2.Components', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('scanoss.api.components.v2.Components', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -101,11 +139,21 @@ def Echo(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/scanoss.api.components.v2.Components/Echo', + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.components.v2.Components/Echo', scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def SearchComponents(request, @@ -118,11 +166,21 @@ def SearchComponents(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/scanoss.api.components.v2.Components/SearchComponents', + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.components.v2.Components/SearchComponents', scanoss_dot_api_dot_components_dot_v2_dot_scanoss__components__pb2.CompSearchRequest.SerializeToString, scanoss_dot_api_dot_components_dot_v2_dot_scanoss__components__pb2.CompSearchResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def GetComponentVersions(request, @@ -135,8 +193,45 @@ def GetComponentVersions(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/scanoss.api.components.v2.Components/GetComponentVersions', + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.components.v2.Components/GetComponentVersions', scanoss_dot_api_dot_components_dot_v2_dot_scanoss__components__pb2.CompVersionRequest.SerializeToString, scanoss_dot_api_dot_components_dot_v2_dot_scanoss__components__pb2.CompVersionResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentStatistics(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.components.v2.Components/GetComponentStatistics', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + scanoss_dot_api_dot_components_dot_v2_dot_scanoss__components__pb2.ComponentsStatisticResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/src/scanoss/api/cryptography/v2/scanoss_cryptography_pb2.py b/src/scanoss/api/cryptography/v2/scanoss_cryptography_pb2.py new file mode 100644 index 00000000..06324447 --- /dev/null +++ b/src/scanoss/api/cryptography/v2/scanoss_cryptography_pb2.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: scanoss/api/cryptography/v2/scanoss-cryptography.proto +# Protobuf Python Version: 6.31.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 0, + '', + 'scanoss/api/cryptography/v2/scanoss-cryptography.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 +from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 +from protoc_gen_openapiv2.options import annotations_pb2 as protoc__gen__openapiv2_dot_options_dot_annotations__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n6scanoss/api/cryptography/v2/scanoss-cryptography.proto\x12\x1bscanoss.api.cryptography.v2\x1a*scanoss/api/common/v2/scanoss-common.proto\x1a\x1cgoogle/api/annotations.proto\x1a.protoc-gen-openapiv2/options/annotations.proto\"0\n\tAlgorithm\x12\x11\n\talgorithm\x18\x01 \x01(\t\x12\x10\n\x08strength\x18\x02 \x01(\t\"\xf7\x01\n\x11\x41lgorithmResponse\x12\x43\n\x05purls\x18\x01 \x03(\x0b\x32\x34.scanoss.api.cryptography.v2.AlgorithmResponse.Purls\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1a\x62\n\x05Purls\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12:\n\nalgorithms\x18\x03 \x03(\x0b\x32&.scanoss.api.cryptography.v2.Algorithm:\x02\x18\x01\"\x85\x01\n\x13\x43omponentAlgorithms\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x13\n\x0brequirement\x18\x03 \x01(\t\x12:\n\nalgorithms\x18\x04 \x03(\x0b\x32&.scanoss.api.cryptography.v2.Algorithm\"\xe0\x04\n\x1c\x43omponentsAlgorithmsResponse\x12\x44\n\ncomponents\x18\x01 \x03(\x0b\x32\x30.scanoss.api.cryptography.v2.ComponentAlgorithms\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse:\xc2\x03\x92\x41\xbe\x03\n\xbb\x03J\xb8\x03{\"components\":[{\"purl\": \"pkg:github/scanoss/engine\", \"requirement\": \">=5.0.0\", \"version\": \"5.0.0\", \"algorithms\": [{\"algorithm\": \"AES\", \"strength\": \"Strong\"}, {\"algorithm\": \"RSA\", \"strength\": \"Strong\"}]}, {\"purl\": \"pkg:github/scanoss/scanoss.py\", \"requirement\": \"~1.30.0\", \"version\": \"v1.30.0\", \"algorithms\": [{\"algorithm\": \"SHA-256\", \"strength\": \"Strong\"}]}], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Algorithms Successfully retrieved\"}}\"\xc0\x03\n\x1b\x43omponentAlgorithmsResponse\x12\x43\n\tcomponent\x18\x01 \x01(\x0b\x32\x30.scanoss.api.cryptography.v2.ComponentAlgorithms\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse:\xa4\x02\x92\x41\xa0\x02\n\x9d\x02J\x9a\x02{\"component\":{\"purl\": \"pkg:github/scanoss/engine\", \"requirement\": \">=5.0.0\", \"version\": \"5.0.0\", \"algorithms\": [{\"algorithm\": \"AES\", \"strength\": \"Strong\"}, {\"algorithm\": \"RSA\", \"strength\": \"Strong\"}]}, \"status\": {\"status\": \"SUCCESS\", \"message\": \"Algorithms Successfully retrieved\"}}\"\x86\x02\n\x19\x41lgorithmsInRangeResponse\x12J\n\x05purls\x18\x01 \x03(\x0b\x32;.scanoss.api.cryptography.v2.AlgorithmsInRangeResponse.Purl\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1a\x62\n\x04Purl\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x10\n\x08versions\x18\x02 \x03(\t\x12:\n\nalgorithms\x18\x03 \x03(\x0b\x32&.scanoss.api.cryptography.v2.Algorithm:\x02\x18\x01\"\xa2\x05\n#ComponentsAlgorithmsInRangeResponse\x12^\n\ncomponents\x18\x01 \x03(\x0b\x32J.scanoss.api.cryptography.v2.ComponentsAlgorithmsInRangeResponse.Component\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1ag\n\tComponent\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x10\n\x08versions\x18\x02 \x03(\t\x12:\n\nalgorithms\x18\x03 \x03(\x0b\x32&.scanoss.api.cryptography.v2.Algorithm:\xfa\x02\x92\x41\xf6\x02\n\xf3\x02J\xf0\x02{\"components\":[{\"purl\": \"pkg:github/scanoss/engine\", \"versions\": [\"1.0.0\", \"2.0.0\"], \"algorithms\": [{\"algorithm\": \"AES\", \"strength\": \"Strong\"}]}, {\"purl\": \"pkg:github/scanoss/scanoss.py\", \"versions\": [\"v1.30.0\"], \"algorithms\": [{\"algorithm\": \"SHA-256\", \"strength\": \"Strong\"}]}], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Algorithms in range Successfully retrieved\"}}\"\xce\x04\n\"ComponentAlgorithmsInRangeResponse\x12\\\n\tcomponent\x18\x01 \x01(\x0b\x32I.scanoss.api.cryptography.v2.ComponentAlgorithmsInRangeResponse.Component\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1ag\n\tComponent\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x10\n\x08versions\x18\x02 \x03(\t\x12:\n\nalgorithms\x18\x03 \x03(\x0b\x32&.scanoss.api.cryptography.v2.Algorithm:\xa9\x02\x92\x41\xa5\x02\n\xa2\x02J\x9f\x02{\"component\": {\"purl\": \"pkg:github/scanoss/engine\", \"versions\": [\"1.0.0\", \"2.0.0\", \"3.0.0\"], \"algorithms\": [{\"algorithm\": \"AES\", \"strength\": \"Strong\"}, {\"algorithm\": \"RSA\", \"strength\": \"Strong\"}]}, \"status\": {\"status\": \"SUCCESS\", \"message\": \"Algorithms in range Successfully retrieved\"}}\"\x86\x02\n\x17VersionsInRangeResponse\x12H\n\x05purls\x18\x01 \x03(\x0b\x32\x39.scanoss.api.cryptography.v2.VersionsInRangeResponse.Purl\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1a\x66\n\x04Purl\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12$\n\rversions_with\x18\x02 \x03(\tR\rversions_with\x12*\n\x10versions_without\x18\x03 \x03(\tR\x10versions_without:\x02\x18\x01\"\xeb\x04\n!ComponentsVersionsInRangeResponse\x12\\\n\ncomponents\x18\x01 \x03(\x0b\x32H.scanoss.api.cryptography.v2.ComponentsVersionsInRangeResponse.Component\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1ak\n\tComponent\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12$\n\rversions_with\x18\x02 \x03(\tR\rversions_with\x12*\n\x10versions_without\x18\x03 \x03(\tR\x10versions_without:\xc3\x02\x92\x41\xbf\x02\n\xbc\x02J\xb9\x02{\"components\":[{\"purl\": \"pkg:github/scanoss/engine\", \"versions_with\": [\"2.0.0\", \"3.0.0\"], \"versions_without\": [\"1.0.0\"]}, {\"purl\": \"pkg:github/scanoss/scanoss.py\", \"versions_with\": [\"v1.30.0\"], \"versions_without\": [\"v1.29.0\"]}], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Version ranges Successfully retrieved\"}}\"\x8e\x04\n ComponentVersionsInRangeResponse\x12Z\n\tcomponent\x18\x01 \x01(\x0b\x32G.scanoss.api.cryptography.v2.ComponentVersionsInRangeResponse.Component\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1ak\n\tComponent\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12$\n\rversions_with\x18\x02 \x03(\tR\rversions_with\x12*\n\x10versions_without\x18\x03 \x03(\tR\x10versions_without:\xe9\x01\x92\x41\xe5\x01\n\xe2\x01J\xdf\x01{\"component\": {\"purl\": \"pkg:github/scanoss/engine\", \"versions_with\": [\"2.0.0\", \"3.0.0\", \"4.0.0\"], \"versions_without\": [\"1.0.0\", \"1.5.0\"]}, \"status\": {\"status\": \"SUCCESS\", \"message\": \"Version ranges Successfully retrieved\"}}\"b\n\x04Hint\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12\x10\n\x08\x63\x61tegory\x18\x04 \x01(\t\x12\x0b\n\x03url\x18\x05 \x01(\t\x12\x0c\n\x04purl\x18\x06 \x01(\t\"v\n\x0e\x43omponentHints\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x13\n\x0brequirement\x18\x03 \x01(\t\x12\x30\n\x05hints\x18\x04 \x03(\x0b\x32!.scanoss.api.cryptography.v2.Hint\"\xe1\x01\n\rHintsResponse\x12?\n\x05purls\x18\x01 \x03(\x0b\x32\x30.scanoss.api.cryptography.v2.HintsResponse.Purls\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1aX\n\x05Purls\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x30\n\x05hints\x18\x03 \x03(\x0b\x32!.scanoss.api.cryptography.v2.Hint\"\xee\x01\n\x14HintsInRangeResponse\x12\x45\n\x05purls\x18\x01 \x03(\x0b\x32\x36.scanoss.api.cryptography.v2.HintsInRangeResponse.Purl\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1aX\n\x04Purl\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x10\n\x08versions\x18\x02 \x03(\t\x12\x30\n\x05hints\x18\x03 \x03(\x0b\x32!.scanoss.api.cryptography.v2.Hint\"\x91\x02\n\x1e\x43omponentsHintsInRangeResponse\x12Y\n\ncomponents\x18\x01 \x03(\x0b\x32\x45.scanoss.api.cryptography.v2.ComponentsHintsInRangeResponse.Component\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1a]\n\tComponent\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x10\n\x08versions\x18\x02 \x03(\t\x12\x30\n\x05hints\x18\x03 \x03(\x0b\x32!.scanoss.api.cryptography.v2.Hint\"\x8e\x02\n\x1d\x43omponentHintsInRangeResponse\x12W\n\tcomponent\x18\x01 \x01(\x0b\x32\x44.scanoss.api.cryptography.v2.ComponentHintsInRangeResponse.Component\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1a]\n\tComponent\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x10\n\x08versions\x18\x02 \x03(\t\x12\x30\n\x05hints\x18\x03 \x03(\x0b\x32!.scanoss.api.cryptography.v2.Hint\"\xd4\x06\n!ComponentsEncryptionHintsResponse\x12?\n\ncomponents\x18\x01 \x03(\x0b\x32+.scanoss.api.cryptography.v2.ComponentHints\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse:\xb6\x05\x92\x41\xb2\x05\n\xaf\x05J\xac\x05{\"components\":[{\"purl\": \"pkg:github/scanoss/engine\", \"requirement\": \">=5.0.0\", \"version\": \"5.0.0\", \"hints\": [{\"id\": \"openssl-hint-001\", \"name\": \"OpenSSL\", \"description\": \"Industry standard cryptographic library\", \"category\": \"library\", \"url\": \"https://www.openssl.org/\", \"purl\": \"pkg:generic/openssl@3.0.0\"}]}, {\"purl\": \"pkg:github/scanoss/scanoss.py\", \"requirement\": \"~1.30.0\", \"version\": \"v1.30.0\", \"hints\": [{\"id\": \"tls-protocol-001\", \"name\": \"TLS 1.3\", \"description\": \"Transport Layer Security protocol\", \"category\": \"protocol\", \"url\": \"https://tools.ietf.org/html/rfc8446\", \"purl\": \"\"}]}], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Cryptographic hints Successfully retrieved\"}}\"\xb4\x04\n ComponentEncryptionHintsResponse\x12>\n\tcomponent\x18\x01 \x01(\x0b\x32+.scanoss.api.cryptography.v2.ComponentHints\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse:\x98\x03\x92\x41\x94\x03\n\x91\x03J\x8e\x03{\"component\":{\"purl\": \"pkg:github/scanoss/engine\", \"requirement\": \">=5.0.0\", \"version\": \"5.0.0\", \"hints\": [{\"id\": \"openssl-hint-001\", \"name\": \"OpenSSL\", \"description\": \"Industry standard cryptographic library\", \"category\": \"library\", \"url\": \"https://www.openssl.org/\", \"purl\": \"pkg:generic/openssl@3.0.0\"}]}, \"status\": {\"status\": \"SUCCESS\", \"message\": \"Cryptographic hints Successfully retrieved\"}}2\x86\x14\n\x0c\x43ryptography\x12q\n\x04\x45\x63ho\x12\".scanoss.api.common.v2.EchoRequest\x1a#.scanoss.api.common.v2.EchoResponse\" \x82\xd3\xe4\x93\x02\x1a\"\x15/v2/cryptography/echo:\x01*\x12h\n\rGetAlgorithms\x12\".scanoss.api.common.v2.PurlRequest\x1a..scanoss.api.cryptography.v2.AlgorithmResponse\"\x03\x88\x02\x01\x12\xaa\x01\n\x16GetComponentAlgorithms\x12\'.scanoss.api.common.v2.ComponentRequest\x1a\x38.scanoss.api.cryptography.v2.ComponentAlgorithmsResponse\"-\x82\xd3\xe4\x93\x02\'\x12%/v2/cryptography/algorithms/component\x12\xb1\x01\n\x17GetComponentsAlgorithms\x12(.scanoss.api.common.v2.ComponentsRequest\x1a\x39.scanoss.api.cryptography.v2.ComponentsAlgorithmsResponse\"1\x82\xd3\xe4\x93\x02+\"&/v2/cryptography/algorithms/components:\x01*\x12w\n\x14GetAlgorithmsInRange\x12\".scanoss.api.common.v2.PurlRequest\x1a\x36.scanoss.api.cryptography.v2.AlgorithmsInRangeResponse\"\x03\x88\x02\x01\x12\xbe\x01\n\x1dGetComponentAlgorithmsInRange\x12\'.scanoss.api.common.v2.ComponentRequest\x1a?.scanoss.api.cryptography.v2.ComponentAlgorithmsInRangeResponse\"3\x82\xd3\xe4\x93\x02-\x12+/v2/cryptography/algorithms/range/component\x12\xc5\x01\n\x1eGetComponentsAlgorithmsInRange\x12(.scanoss.api.common.v2.ComponentsRequest\x1a@.scanoss.api.cryptography.v2.ComponentsAlgorithmsInRangeResponse\"7\x82\xd3\xe4\x93\x02\x31\",/v2/cryptography/algorithms/range/components:\x01*\x12s\n\x12GetVersionsInRange\x12\".scanoss.api.common.v2.PurlRequest\x1a\x34.scanoss.api.cryptography.v2.VersionsInRangeResponse\"\x03\x88\x02\x01\x12\xc3\x01\n\x1bGetComponentVersionsInRange\x12\'.scanoss.api.common.v2.ComponentRequest\x1a=.scanoss.api.cryptography.v2.ComponentVersionsInRangeResponse\"<\x82\xd3\xe4\x93\x02\x36\x12\x34/v2/cryptography/algorithms/versions/range/component\x12\xca\x01\n\x1cGetComponentsVersionsInRange\x12(.scanoss.api.common.v2.ComponentsRequest\x1a>.scanoss.api.cryptography.v2.ComponentsVersionsInRangeResponse\"@\x82\xd3\xe4\x93\x02:\"5/v2/cryptography/algorithms/versions/range/components:\x01*\x12m\n\x0fGetHintsInRange\x12\".scanoss.api.common.v2.PurlRequest\x1a\x31.scanoss.api.cryptography.v2.HintsInRangeResponse\"\x03\x88\x02\x01\x12\xaf\x01\n\x18GetComponentHintsInRange\x12\'.scanoss.api.common.v2.ComponentRequest\x1a:.scanoss.api.cryptography.v2.ComponentHintsInRangeResponse\".\x82\xd3\xe4\x93\x02(\x12&/v2/cryptography/hints/range/component\x12\xb6\x01\n\x19GetComponentsHintsInRange\x12(.scanoss.api.common.v2.ComponentsRequest\x1a;.scanoss.api.cryptography.v2.ComponentsHintsInRangeResponse\"2\x82\xd3\xe4\x93\x02,\"\'/v2/cryptography/hints/range/components:\x01*\x12i\n\x12GetEncryptionHints\x12\".scanoss.api.common.v2.PurlRequest\x1a*.scanoss.api.cryptography.v2.HintsResponse\"\x03\x88\x02\x01\x12\xaf\x01\n\x1bGetComponentEncryptionHints\x12\'.scanoss.api.common.v2.ComponentRequest\x1a=.scanoss.api.cryptography.v2.ComponentEncryptionHintsResponse\"(\x82\xd3\xe4\x93\x02\"\x12 /v2/cryptography/hints/component\x12\xb6\x01\n\x1cGetComponentsEncryptionHints\x12(.scanoss.api.common.v2.ComponentsRequest\x1a>.scanoss.api.cryptography.v2.ComponentsEncryptionHintsResponse\",\x82\xd3\xe4\x93\x02&\"!/v2/cryptography/hints/components:\x01*B\xbb\x03Z9github.amrom.workers.dev/scanoss/papi/api/cryptographyv2;cryptographyv2\x92\x41\xfc\x02\x12\x83\x02\n\x1cSCANOSS Cryptography Service\x12\x87\x01\x43ryptography service provides cryptographic intelligence for software components including algorithm detection and encryption analysis.\"T\n\x14scanoss-cryptography\x12\'https://github.com/scanoss/cryptography\x1a\x13support@scanoss.com2\x03\x32.0\x1a\x0f\x61pi.scanoss.com*\x02\x01\x02\x32\x10\x61pplication/json:\x10\x61pplication/jsonR;\n\x03\x34\x30\x34\x12\x34\n*Returned when the resource does not exist.\x12\x06\n\x04\x9a\x02\x01\x07\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'scanoss.api.cryptography.v2.scanoss_cryptography_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z9github.amrom.workers.dev/scanoss/papi/api/cryptographyv2;cryptographyv2\222A\374\002\022\203\002\n\034SCANOSS Cryptography Service\022\207\001Cryptography service provides cryptographic intelligence for software components including algorithm detection and encryption analysis.\"T\n\024scanoss-cryptography\022\'https://github.com/scanoss/cryptography\032\023support@scanoss.com2\0032.0\032\017api.scanoss.com*\002\001\0022\020application/json:\020application/jsonR;\n\003404\0224\n*Returned when the resource does not exist.\022\006\n\004\232\002\001\007' + _globals['_ALGORITHMRESPONSE']._loaded_options = None + _globals['_ALGORITHMRESPONSE']._serialized_options = b'\030\001' + _globals['_COMPONENTSALGORITHMSRESPONSE']._loaded_options = None + _globals['_COMPONENTSALGORITHMSRESPONSE']._serialized_options = b'\222A\276\003\n\273\003J\270\003{\"components\":[{\"purl\": \"pkg:github/scanoss/engine\", \"requirement\": \">=5.0.0\", \"version\": \"5.0.0\", \"algorithms\": [{\"algorithm\": \"AES\", \"strength\": \"Strong\"}, {\"algorithm\": \"RSA\", \"strength\": \"Strong\"}]}, {\"purl\": \"pkg:github/scanoss/scanoss.py\", \"requirement\": \"~1.30.0\", \"version\": \"v1.30.0\", \"algorithms\": [{\"algorithm\": \"SHA-256\", \"strength\": \"Strong\"}]}], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Algorithms Successfully retrieved\"}}' + _globals['_COMPONENTALGORITHMSRESPONSE']._loaded_options = None + _globals['_COMPONENTALGORITHMSRESPONSE']._serialized_options = b'\222A\240\002\n\235\002J\232\002{\"component\":{\"purl\": \"pkg:github/scanoss/engine\", \"requirement\": \">=5.0.0\", \"version\": \"5.0.0\", \"algorithms\": [{\"algorithm\": \"AES\", \"strength\": \"Strong\"}, {\"algorithm\": \"RSA\", \"strength\": \"Strong\"}]}, \"status\": {\"status\": \"SUCCESS\", \"message\": \"Algorithms Successfully retrieved\"}}' + _globals['_ALGORITHMSINRANGERESPONSE']._loaded_options = None + _globals['_ALGORITHMSINRANGERESPONSE']._serialized_options = b'\030\001' + _globals['_COMPONENTSALGORITHMSINRANGERESPONSE']._loaded_options = None + _globals['_COMPONENTSALGORITHMSINRANGERESPONSE']._serialized_options = b'\222A\366\002\n\363\002J\360\002{\"components\":[{\"purl\": \"pkg:github/scanoss/engine\", \"versions\": [\"1.0.0\", \"2.0.0\"], \"algorithms\": [{\"algorithm\": \"AES\", \"strength\": \"Strong\"}]}, {\"purl\": \"pkg:github/scanoss/scanoss.py\", \"versions\": [\"v1.30.0\"], \"algorithms\": [{\"algorithm\": \"SHA-256\", \"strength\": \"Strong\"}]}], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Algorithms in range Successfully retrieved\"}}' + _globals['_COMPONENTALGORITHMSINRANGERESPONSE']._loaded_options = None + _globals['_COMPONENTALGORITHMSINRANGERESPONSE']._serialized_options = b'\222A\245\002\n\242\002J\237\002{\"component\": {\"purl\": \"pkg:github/scanoss/engine\", \"versions\": [\"1.0.0\", \"2.0.0\", \"3.0.0\"], \"algorithms\": [{\"algorithm\": \"AES\", \"strength\": \"Strong\"}, {\"algorithm\": \"RSA\", \"strength\": \"Strong\"}]}, \"status\": {\"status\": \"SUCCESS\", \"message\": \"Algorithms in range Successfully retrieved\"}}' + _globals['_VERSIONSINRANGERESPONSE']._loaded_options = None + _globals['_VERSIONSINRANGERESPONSE']._serialized_options = b'\030\001' + _globals['_COMPONENTSVERSIONSINRANGERESPONSE']._loaded_options = None + _globals['_COMPONENTSVERSIONSINRANGERESPONSE']._serialized_options = b'\222A\277\002\n\274\002J\271\002{\"components\":[{\"purl\": \"pkg:github/scanoss/engine\", \"versions_with\": [\"2.0.0\", \"3.0.0\"], \"versions_without\": [\"1.0.0\"]}, {\"purl\": \"pkg:github/scanoss/scanoss.py\", \"versions_with\": [\"v1.30.0\"], \"versions_without\": [\"v1.29.0\"]}], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Version ranges Successfully retrieved\"}}' + _globals['_COMPONENTVERSIONSINRANGERESPONSE']._loaded_options = None + _globals['_COMPONENTVERSIONSINRANGERESPONSE']._serialized_options = b'\222A\345\001\n\342\001J\337\001{\"component\": {\"purl\": \"pkg:github/scanoss/engine\", \"versions_with\": [\"2.0.0\", \"3.0.0\", \"4.0.0\"], \"versions_without\": [\"1.0.0\", \"1.5.0\"]}, \"status\": {\"status\": \"SUCCESS\", \"message\": \"Version ranges Successfully retrieved\"}}' + _globals['_COMPONENTSENCRYPTIONHINTSRESPONSE']._loaded_options = None + _globals['_COMPONENTSENCRYPTIONHINTSRESPONSE']._serialized_options = b'\222A\262\005\n\257\005J\254\005{\"components\":[{\"purl\": \"pkg:github/scanoss/engine\", \"requirement\": \">=5.0.0\", \"version\": \"5.0.0\", \"hints\": [{\"id\": \"openssl-hint-001\", \"name\": \"OpenSSL\", \"description\": \"Industry standard cryptographic library\", \"category\": \"library\", \"url\": \"https://www.openssl.org/\", \"purl\": \"pkg:generic/openssl@3.0.0\"}]}, {\"purl\": \"pkg:github/scanoss/scanoss.py\", \"requirement\": \"~1.30.0\", \"version\": \"v1.30.0\", \"hints\": [{\"id\": \"tls-protocol-001\", \"name\": \"TLS 1.3\", \"description\": \"Transport Layer Security protocol\", \"category\": \"protocol\", \"url\": \"https://tools.ietf.org/html/rfc8446\", \"purl\": \"\"}]}], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Cryptographic hints Successfully retrieved\"}}' + _globals['_COMPONENTENCRYPTIONHINTSRESPONSE']._loaded_options = None + _globals['_COMPONENTENCRYPTIONHINTSRESPONSE']._serialized_options = b'\222A\224\003\n\221\003J\216\003{\"component\":{\"purl\": \"pkg:github/scanoss/engine\", \"requirement\": \">=5.0.0\", \"version\": \"5.0.0\", \"hints\": [{\"id\": \"openssl-hint-001\", \"name\": \"OpenSSL\", \"description\": \"Industry standard cryptographic library\", \"category\": \"library\", \"url\": \"https://www.openssl.org/\", \"purl\": \"pkg:generic/openssl@3.0.0\"}]}, \"status\": {\"status\": \"SUCCESS\", \"message\": \"Cryptographic hints Successfully retrieved\"}}' + _globals['_CRYPTOGRAPHY'].methods_by_name['Echo']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['Echo']._serialized_options = b'\202\323\344\223\002\032\"\025/v2/cryptography/echo:\001*' + _globals['_CRYPTOGRAPHY'].methods_by_name['GetAlgorithms']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['GetAlgorithms']._serialized_options = b'\210\002\001' + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentAlgorithms']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentAlgorithms']._serialized_options = b'\202\323\344\223\002\'\022%/v2/cryptography/algorithms/component' + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentsAlgorithms']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentsAlgorithms']._serialized_options = b'\202\323\344\223\002+\"&/v2/cryptography/algorithms/components:\001*' + _globals['_CRYPTOGRAPHY'].methods_by_name['GetAlgorithmsInRange']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['GetAlgorithmsInRange']._serialized_options = b'\210\002\001' + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentAlgorithmsInRange']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentAlgorithmsInRange']._serialized_options = b'\202\323\344\223\002-\022+/v2/cryptography/algorithms/range/component' + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentsAlgorithmsInRange']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentsAlgorithmsInRange']._serialized_options = b'\202\323\344\223\0021\",/v2/cryptography/algorithms/range/components:\001*' + _globals['_CRYPTOGRAPHY'].methods_by_name['GetVersionsInRange']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['GetVersionsInRange']._serialized_options = b'\210\002\001' + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentVersionsInRange']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentVersionsInRange']._serialized_options = b'\202\323\344\223\0026\0224/v2/cryptography/algorithms/versions/range/component' + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentsVersionsInRange']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentsVersionsInRange']._serialized_options = b'\202\323\344\223\002:\"5/v2/cryptography/algorithms/versions/range/components:\001*' + _globals['_CRYPTOGRAPHY'].methods_by_name['GetHintsInRange']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['GetHintsInRange']._serialized_options = b'\210\002\001' + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentHintsInRange']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentHintsInRange']._serialized_options = b'\202\323\344\223\002(\022&/v2/cryptography/hints/range/component' + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentsHintsInRange']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentsHintsInRange']._serialized_options = b'\202\323\344\223\002,\"\'/v2/cryptography/hints/range/components:\001*' + _globals['_CRYPTOGRAPHY'].methods_by_name['GetEncryptionHints']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['GetEncryptionHints']._serialized_options = b'\210\002\001' + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentEncryptionHints']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentEncryptionHints']._serialized_options = b'\202\323\344\223\002\"\022 /v2/cryptography/hints/component' + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentsEncryptionHints']._loaded_options = None + _globals['_CRYPTOGRAPHY'].methods_by_name['GetComponentsEncryptionHints']._serialized_options = b'\202\323\344\223\002&\"!/v2/cryptography/hints/components:\001*' + _globals['_ALGORITHM']._serialized_start=209 + _globals['_ALGORITHM']._serialized_end=257 + _globals['_ALGORITHMRESPONSE']._serialized_start=260 + _globals['_ALGORITHMRESPONSE']._serialized_end=507 + _globals['_ALGORITHMRESPONSE_PURLS']._serialized_start=405 + _globals['_ALGORITHMRESPONSE_PURLS']._serialized_end=503 + _globals['_COMPONENTALGORITHMS']._serialized_start=510 + _globals['_COMPONENTALGORITHMS']._serialized_end=643 + _globals['_COMPONENTSALGORITHMSRESPONSE']._serialized_start=646 + _globals['_COMPONENTSALGORITHMSRESPONSE']._serialized_end=1254 + _globals['_COMPONENTALGORITHMSRESPONSE']._serialized_start=1257 + _globals['_COMPONENTALGORITHMSRESPONSE']._serialized_end=1705 + _globals['_ALGORITHMSINRANGERESPONSE']._serialized_start=1708 + _globals['_ALGORITHMSINRANGERESPONSE']._serialized_end=1970 + _globals['_ALGORITHMSINRANGERESPONSE_PURL']._serialized_start=1868 + _globals['_ALGORITHMSINRANGERESPONSE_PURL']._serialized_end=1966 + _globals['_COMPONENTSALGORITHMSINRANGERESPONSE']._serialized_start=1973 + _globals['_COMPONENTSALGORITHMSINRANGERESPONSE']._serialized_end=2647 + _globals['_COMPONENTSALGORITHMSINRANGERESPONSE_COMPONENT']._serialized_start=2163 + _globals['_COMPONENTSALGORITHMSINRANGERESPONSE_COMPONENT']._serialized_end=2266 + _globals['_COMPONENTALGORITHMSINRANGERESPONSE']._serialized_start=2650 + _globals['_COMPONENTALGORITHMSINRANGERESPONSE']._serialized_end=3240 + _globals['_COMPONENTALGORITHMSINRANGERESPONSE_COMPONENT']._serialized_start=2163 + _globals['_COMPONENTALGORITHMSINRANGERESPONSE_COMPONENT']._serialized_end=2266 + _globals['_VERSIONSINRANGERESPONSE']._serialized_start=3243 + _globals['_VERSIONSINRANGERESPONSE']._serialized_end=3505 + _globals['_VERSIONSINRANGERESPONSE_PURL']._serialized_start=3399 + _globals['_VERSIONSINRANGERESPONSE_PURL']._serialized_end=3501 + _globals['_COMPONENTSVERSIONSINRANGERESPONSE']._serialized_start=3508 + _globals['_COMPONENTSVERSIONSINRANGERESPONSE']._serialized_end=4127 + _globals['_COMPONENTSVERSIONSINRANGERESPONSE_COMPONENT']._serialized_start=3694 + _globals['_COMPONENTSVERSIONSINRANGERESPONSE_COMPONENT']._serialized_end=3801 + _globals['_COMPONENTVERSIONSINRANGERESPONSE']._serialized_start=4130 + _globals['_COMPONENTVERSIONSINRANGERESPONSE']._serialized_end=4656 + _globals['_COMPONENTVERSIONSINRANGERESPONSE_COMPONENT']._serialized_start=3694 + _globals['_COMPONENTVERSIONSINRANGERESPONSE_COMPONENT']._serialized_end=3801 + _globals['_HINT']._serialized_start=4658 + _globals['_HINT']._serialized_end=4756 + _globals['_COMPONENTHINTS']._serialized_start=4758 + _globals['_COMPONENTHINTS']._serialized_end=4876 + _globals['_HINTSRESPONSE']._serialized_start=4879 + _globals['_HINTSRESPONSE']._serialized_end=5104 + _globals['_HINTSRESPONSE_PURLS']._serialized_start=5016 + _globals['_HINTSRESPONSE_PURLS']._serialized_end=5104 + _globals['_HINTSINRANGERESPONSE']._serialized_start=5107 + _globals['_HINTSINRANGERESPONSE']._serialized_end=5345 + _globals['_HINTSINRANGERESPONSE_PURL']._serialized_start=5257 + _globals['_HINTSINRANGERESPONSE_PURL']._serialized_end=5345 + _globals['_COMPONENTSHINTSINRANGERESPONSE']._serialized_start=5348 + _globals['_COMPONENTSHINTSINRANGERESPONSE']._serialized_end=5621 + _globals['_COMPONENTSHINTSINRANGERESPONSE_COMPONENT']._serialized_start=5528 + _globals['_COMPONENTSHINTSINRANGERESPONSE_COMPONENT']._serialized_end=5621 + _globals['_COMPONENTHINTSINRANGERESPONSE']._serialized_start=5624 + _globals['_COMPONENTHINTSINRANGERESPONSE']._serialized_end=5894 + _globals['_COMPONENTHINTSINRANGERESPONSE_COMPONENT']._serialized_start=5528 + _globals['_COMPONENTHINTSINRANGERESPONSE_COMPONENT']._serialized_end=5621 + _globals['_COMPONENTSENCRYPTIONHINTSRESPONSE']._serialized_start=5897 + _globals['_COMPONENTSENCRYPTIONHINTSRESPONSE']._serialized_end=6749 + _globals['_COMPONENTENCRYPTIONHINTSRESPONSE']._serialized_start=6752 + _globals['_COMPONENTENCRYPTIONHINTSRESPONSE']._serialized_end=7316 + _globals['_CRYPTOGRAPHY']._serialized_start=7319 + _globals['_CRYPTOGRAPHY']._serialized_end=9885 +# @@protoc_insertion_point(module_scope) diff --git a/src/scanoss/api/cryptography/v2/scanoss_cryptography_pb2_grpc.py b/src/scanoss/api/cryptography/v2/scanoss_cryptography_pb2_grpc.py new file mode 100644 index 00000000..5aa7f6ad --- /dev/null +++ b/src/scanoss/api/cryptography/v2/scanoss_cryptography_pb2_grpc.py @@ -0,0 +1,861 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 +from scanoss.api.cryptography.v2 import scanoss_cryptography_pb2 as scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2 + +GRPC_GENERATED_VERSION = '1.73.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in scanoss/api/cryptography/v2/scanoss_cryptography_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class CryptographyStub(object): + """ + Cryptography Service Definition + + Provides comprehensive cryptographic intelligence for software components + including algorithm detection, encryption hints, and security assessments. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Echo = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/Echo', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, + _registered_method=True) + self.GetAlgorithms = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/GetAlgorithms', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.AlgorithmResponse.FromString, + _registered_method=True) + self.GetComponentAlgorithms = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/GetComponentAlgorithms', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentAlgorithmsResponse.FromString, + _registered_method=True) + self.GetComponentsAlgorithms = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/GetComponentsAlgorithms', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentsAlgorithmsResponse.FromString, + _registered_method=True) + self.GetAlgorithmsInRange = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/GetAlgorithmsInRange', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.AlgorithmsInRangeResponse.FromString, + _registered_method=True) + self.GetComponentAlgorithmsInRange = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/GetComponentAlgorithmsInRange', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentAlgorithmsInRangeResponse.FromString, + _registered_method=True) + self.GetComponentsAlgorithmsInRange = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/GetComponentsAlgorithmsInRange', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentsAlgorithmsInRangeResponse.FromString, + _registered_method=True) + self.GetVersionsInRange = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/GetVersionsInRange', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.VersionsInRangeResponse.FromString, + _registered_method=True) + self.GetComponentVersionsInRange = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/GetComponentVersionsInRange', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentVersionsInRangeResponse.FromString, + _registered_method=True) + self.GetComponentsVersionsInRange = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/GetComponentsVersionsInRange', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentsVersionsInRangeResponse.FromString, + _registered_method=True) + self.GetHintsInRange = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/GetHintsInRange', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.HintsInRangeResponse.FromString, + _registered_method=True) + self.GetComponentHintsInRange = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/GetComponentHintsInRange', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentHintsInRangeResponse.FromString, + _registered_method=True) + self.GetComponentsHintsInRange = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/GetComponentsHintsInRange', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentsHintsInRangeResponse.FromString, + _registered_method=True) + self.GetEncryptionHints = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/GetEncryptionHints', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.HintsResponse.FromString, + _registered_method=True) + self.GetComponentEncryptionHints = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/GetComponentEncryptionHints', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentEncryptionHintsResponse.FromString, + _registered_method=True) + self.GetComponentsEncryptionHints = channel.unary_unary( + '/scanoss.api.cryptography.v2.Cryptography/GetComponentsEncryptionHints', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentsEncryptionHintsResponse.FromString, + _registered_method=True) + + +class CryptographyServicer(object): + """ + Cryptography Service Definition + + Provides comprehensive cryptographic intelligence for software components + including algorithm detection, encryption hints, and security assessments. + """ + + def Echo(self, request, context): + """ + Returns the same message that was sent, used for health checks and connectivity testing + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetAlgorithms(self, request, context): + """****** Algorithms ****** // + + + Get cryptographic algorithms associated with a list of PURLs - legacy endpoint. + + Legacy method for retrieving cryptographic algorithms used by software components. + Use GetComponentAlgorithms or GetComponentsAlgorithms instead. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentAlgorithms(self, request, context): + """ + Get cryptographic algorithms associated with a single software component. + + Analyzes the component and returns cryptographic algorithms detected in the codebase + including algorithm names and strength classifications. + + See: https://github.com/scanoss/papi/blob/main/protobuf/scanoss/api/cryptography/v2/README.md#getcomponentalgorithms + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentsAlgorithms(self, request, context): + """ + Get cryptographic algorithms associated with multiple software components in a single request. + + Analyzes multiple components and returns cryptographic algorithms detected in each codebase + including algorithm names and strength classifications. + + See: https://github.com/scanoss/papi/blob/main/protobuf/scanoss/api/cryptography/v2/README.md#getcomponentsalgorithms + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetAlgorithmsInRange(self, request, context): + """****** Algorithms in range ****** // + + + Get cryptographic algorithms used across version ranges - legacy endpoint. + + Legacy method for retrieving cryptographic algorithms across component version ranges. + Use GetComponentAlgorithmsInRange or GetComponentsAlgorithmsInRange instead. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentAlgorithmsInRange(self, request, context): + """ + Get cryptographic algorithms used by a component across specified version ranges. + + Analyzes the component across version ranges and returns all cryptographic algorithms + detected along with the versions where they appear. + + See: https://github.com/scanoss/papi/blob/main/protobuf/scanoss/api/cryptography/v2/README.md#getcomponentalgorithmsinrange + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentsAlgorithmsInRange(self, request, context): + """ + Get cryptographic algorithms used by multiple components across specified version ranges. + + Analyzes multiple components across version ranges and returns all cryptographic algorithms + detected along with the versions where they appear for each component. + + See: https://github.com/scanoss/papi/blob/main/protobuf/scanoss/api/cryptography/v2/README.md#getcomponentsalgorithmsinrange + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetVersionsInRange(self, request, context): + """****** Versions in range ****** // + + + Get component versions that contain or don't contain cryptographic algorithms - legacy endpoint. + + Legacy method for retrieving version lists based on cryptographic algorithm presence. + Use ComponentVersionsInRange or ComponentsVersionsInRange instead. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentVersionsInRange(self, request, context): + """ + Get component versions that contain or don't contain cryptographic algorithms within specified ranges. + + Returns lists of versions that either contain cryptographic algorithms or don't, + helping assess cryptographic presence across component evolution. + + See: https://github.com/scanoss/papi/blob/main/protobuf/scanoss/api/cryptography/v2/README.md#componentversionsinrange + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentsVersionsInRange(self, request, context): + """ + Get multiple component versions that contain or don't contain cryptographic algorithms within specified ranges. + + Returns lists of versions for multiple components that either contain cryptographic algorithms or don't, + helping assess cryptographic presence across component evolution in batch operations. + + See: https://github.com/scanoss/papi/blob/main/protobuf/scanoss/api/cryptography/v2/README.md#componentsversionsinrange + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetHintsInRange(self, request, context): + """****** Hints in range ****** // + + + Get cryptographic hints across version ranges - legacy endpoint. + + Legacy method for retrieving cryptographic hints related to protocols, libraries, + SDKs and frameworks across version ranges. Use ComponentHintsInRange or ComponentsHintsInRange instead. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentHintsInRange(self, request, context): + """ + Get cryptographic hints across version ranges - legacy endpoint. + + Legacy method for retrieving cryptographic hints related to protocols, libraries, + SDKs and frameworks across version ranges. Use ComponentHintsInRange or ComponentsHintsInRange instead. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentsHintsInRange(self, request, context): + """ + Get cryptographic hints across version ranges - legacy endpoint. + + Legacy method for retrieving cryptographic hints related to protocols, libraries, + SDKs and frameworks across version ranges. Use ComponentHintsInRange or ComponentsHintsInRange instead. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetEncryptionHints(self, request, context): + """****** Encryption hints ****** // + + + Get encryption hints for components - legacy endpoint. + + Legacy method for retrieving hints about cryptographic protocols, libraries, + SDKs and frameworks used by components. Use ComponentHintsInRange or ComponentsHintsInRange instead. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentEncryptionHints(self, request, context): + """ + Get cryptographic hints for a single component. + + Returns hints about cryptographic protocols, libraries, SDKs and frameworks + used by the component, providing insights into cryptographic dependencies. + + See: https://github.com/scanoss/papi/blob/main/protobuf/scanoss/api/cryptography/v2/README.md#componenthintsinrange + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentsEncryptionHints(self, request, context): + """ + Get cryptographic hints for multiple components in a single request. + + Returns hints about cryptographic protocols, libraries, SDKs and frameworks + used by multiple components, providing insights into cryptographic dependencies. + + See: https://github.com/scanoss/papi/blob/main/protobuf/scanoss/api/cryptography/v2/README.md#componentshintsinrange + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_CryptographyServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Echo': grpc.unary_unary_rpc_method_handler( + servicer.Echo, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.FromString, + response_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.SerializeToString, + ), + 'GetAlgorithms': grpc.unary_unary_rpc_method_handler( + servicer.GetAlgorithms, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.FromString, + response_serializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.AlgorithmResponse.SerializeToString, + ), + 'GetComponentAlgorithms': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentAlgorithms, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.FromString, + response_serializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentAlgorithmsResponse.SerializeToString, + ), + 'GetComponentsAlgorithms': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentsAlgorithms, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.FromString, + response_serializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentsAlgorithmsResponse.SerializeToString, + ), + 'GetAlgorithmsInRange': grpc.unary_unary_rpc_method_handler( + servicer.GetAlgorithmsInRange, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.FromString, + response_serializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.AlgorithmsInRangeResponse.SerializeToString, + ), + 'GetComponentAlgorithmsInRange': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentAlgorithmsInRange, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.FromString, + response_serializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentAlgorithmsInRangeResponse.SerializeToString, + ), + 'GetComponentsAlgorithmsInRange': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentsAlgorithmsInRange, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.FromString, + response_serializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentsAlgorithmsInRangeResponse.SerializeToString, + ), + 'GetVersionsInRange': grpc.unary_unary_rpc_method_handler( + servicer.GetVersionsInRange, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.FromString, + response_serializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.VersionsInRangeResponse.SerializeToString, + ), + 'GetComponentVersionsInRange': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentVersionsInRange, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.FromString, + response_serializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentVersionsInRangeResponse.SerializeToString, + ), + 'GetComponentsVersionsInRange': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentsVersionsInRange, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.FromString, + response_serializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentsVersionsInRangeResponse.SerializeToString, + ), + 'GetHintsInRange': grpc.unary_unary_rpc_method_handler( + servicer.GetHintsInRange, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.FromString, + response_serializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.HintsInRangeResponse.SerializeToString, + ), + 'GetComponentHintsInRange': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentHintsInRange, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.FromString, + response_serializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentHintsInRangeResponse.SerializeToString, + ), + 'GetComponentsHintsInRange': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentsHintsInRange, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.FromString, + response_serializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentsHintsInRangeResponse.SerializeToString, + ), + 'GetEncryptionHints': grpc.unary_unary_rpc_method_handler( + servicer.GetEncryptionHints, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.FromString, + response_serializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.HintsResponse.SerializeToString, + ), + 'GetComponentEncryptionHints': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentEncryptionHints, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.FromString, + response_serializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentEncryptionHintsResponse.SerializeToString, + ), + 'GetComponentsEncryptionHints': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentsEncryptionHints, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.FromString, + response_serializer=scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentsEncryptionHintsResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'scanoss.api.cryptography.v2.Cryptography', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('scanoss.api.cryptography.v2.Cryptography', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class Cryptography(object): + """ + Cryptography Service Definition + + Provides comprehensive cryptographic intelligence for software components + including algorithm detection, encryption hints, and security assessments. + """ + + @staticmethod + def Echo(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/Echo', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetAlgorithms(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/GetAlgorithms', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.AlgorithmResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentAlgorithms(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/GetComponentAlgorithms', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentAlgorithmsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentsAlgorithms(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/GetComponentsAlgorithms', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentsAlgorithmsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetAlgorithmsInRange(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/GetAlgorithmsInRange', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.AlgorithmsInRangeResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentAlgorithmsInRange(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/GetComponentAlgorithmsInRange', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentAlgorithmsInRangeResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentsAlgorithmsInRange(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/GetComponentsAlgorithmsInRange', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentsAlgorithmsInRangeResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetVersionsInRange(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/GetVersionsInRange', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.VersionsInRangeResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentVersionsInRange(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/GetComponentVersionsInRange', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentVersionsInRangeResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentsVersionsInRange(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/GetComponentsVersionsInRange', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentsVersionsInRangeResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetHintsInRange(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/GetHintsInRange', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.HintsInRangeResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentHintsInRange(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/GetComponentHintsInRange', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentHintsInRangeResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentsHintsInRange(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/GetComponentsHintsInRange', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentsHintsInRangeResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetEncryptionHints(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/GetEncryptionHints', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.HintsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentEncryptionHints(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/GetComponentEncryptionHints', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentEncryptionHintsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentsEncryptionHints(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.cryptography.v2.Cryptography/GetComponentsEncryptionHints', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + scanoss_dot_api_dot_cryptography_dot_v2_dot_scanoss__cryptography__pb2.ComponentsEncryptionHintsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/src/scanoss/api/dependencies/__init__.py b/src/scanoss/api/dependencies/__init__.py index d2ed05b0..c67e64da 100644 --- a/src/scanoss/api/dependencies/__init__.py +++ b/src/scanoss/api/dependencies/__init__.py @@ -1,23 +1,23 @@ """ - SPDX-License-Identifier: MIT +SPDX-License-Identifier: MIT - Copyright (c) 2021, SCANOSS + Copyright (c) 2021, SCANOSS - 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: + 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 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. + 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/src/scanoss/api/dependencies/v2/__init__.py b/src/scanoss/api/dependencies/v2/__init__.py index d2ed05b0..c67e64da 100644 --- a/src/scanoss/api/dependencies/v2/__init__.py +++ b/src/scanoss/api/dependencies/v2/__init__.py @@ -1,23 +1,23 @@ """ - SPDX-License-Identifier: MIT +SPDX-License-Identifier: MIT - Copyright (c) 2021, SCANOSS + Copyright (c) 2021, SCANOSS - 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: + 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 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. + 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/src/scanoss/api/dependencies/v2/scanoss_dependencies_pb2.py b/src/scanoss/api/dependencies/v2/scanoss_dependencies_pb2.py index 12986d77..37662af9 100644 --- a/src/scanoss/api/dependencies/v2/scanoss_dependencies_pb2.py +++ b/src/scanoss/api/dependencies/v2/scanoss_dependencies_pb2.py @@ -1,460 +1,74 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: scanoss/api/dependencies/v2/scanoss-dependencies.proto +# Protobuf Python Version: 6.31.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 0, + '', + 'scanoss/api/dependencies/v2/scanoss-dependencies.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='scanoss/api/dependencies/v2/scanoss-dependencies.proto', - package='scanoss.api.dependencies.v2', - syntax='proto3', - serialized_options=b'Z9github.amrom.workers.dev/scanoss/papi/api/dependenciesv2;dependenciesv2', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n6scanoss/api/dependencies/v2/scanoss-dependencies.proto\x12\x1bscanoss.api.dependencies.v2\x1a*scanoss/api/common/v2/scanoss-common.proto\"\xef\x01\n\x11\x44\x65pendencyRequest\x12\x43\n\x05\x66iles\x18\x01 \x03(\x0b\x32\x34.scanoss.api.dependencies.v2.DependencyRequest.Files\x12\r\n\x05\x64\x65pth\x18\x02 \x01(\x05\x1a*\n\x05Purls\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x13\n\x0brequirement\x18\x02 \x01(\t\x1aZ\n\x05\x46iles\x12\x0c\n\x04\x66ile\x18\x01 \x01(\t\x12\x43\n\x05purls\x18\x02 \x03(\x0b\x32\x34.scanoss.api.dependencies.v2.DependencyRequest.Purls\"\x98\x04\n\x12\x44\x65pendencyResponse\x12\x44\n\x05\x66iles\x18\x01 \x03(\x0b\x32\x35.scanoss.api.dependencies.v2.DependencyResponse.Files\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1aP\n\x08Licenses\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07spdx_id\x18\x02 \x01(\t\x12\x18\n\x10is_spdx_approved\x18\x03 \x01(\x08\x12\x0b\n\x03url\x18\x04 \x01(\t\x1a\xaa\x01\n\x0c\x44\x65pendencies\x12\x11\n\tcomponent\x18\x01 \x01(\t\x12\x0c\n\x04purl\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12J\n\x08licenses\x18\x04 \x03(\x0b\x32\x38.scanoss.api.dependencies.v2.DependencyResponse.Licenses\x12\x0b\n\x03url\x18\x05 \x01(\t\x12\x0f\n\x07\x63omment\x18\x06 \x01(\t\x1a\x85\x01\n\x05\x46iles\x12\x0c\n\x04\x66ile\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\x12R\n\x0c\x64\x65pendencies\x18\x04 \x03(\x0b\x32<.scanoss.api.dependencies.v2.DependencyResponse.Dependencies2\xd3\x01\n\x0c\x44\x65pendencies\x12O\n\x04\x45\x63ho\x12\".scanoss.api.common.v2.EchoRequest\x1a#.scanoss.api.common.v2.EchoResponse\x12r\n\x0fGetDependencies\x12..scanoss.api.dependencies.v2.DependencyRequest\x1a/.scanoss.api.dependencies.v2.DependencyResponseB;Z9github.amrom.workers.dev/scanoss/papi/api/dependenciesv2;dependenciesv2b\x06proto3' - , - dependencies=[scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.DESCRIPTOR,]) - - - - -_DEPENDENCYREQUEST_PURLS = _descriptor.Descriptor( - name='Purls', - full_name='scanoss.api.dependencies.v2.DependencyRequest.Purls', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='purl', full_name='scanoss.api.dependencies.v2.DependencyRequest.Purls.purl', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='requirement', full_name='scanoss.api.dependencies.v2.DependencyRequest.Purls.requirement', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=237, - serialized_end=279, -) - -_DEPENDENCYREQUEST_FILES = _descriptor.Descriptor( - name='Files', - full_name='scanoss.api.dependencies.v2.DependencyRequest.Files', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='file', full_name='scanoss.api.dependencies.v2.DependencyRequest.Files.file', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='purls', full_name='scanoss.api.dependencies.v2.DependencyRequest.Files.purls', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=281, - serialized_end=371, -) - -_DEPENDENCYREQUEST = _descriptor.Descriptor( - name='DependencyRequest', - full_name='scanoss.api.dependencies.v2.DependencyRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='files', full_name='scanoss.api.dependencies.v2.DependencyRequest.files', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='depth', full_name='scanoss.api.dependencies.v2.DependencyRequest.depth', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_DEPENDENCYREQUEST_PURLS, _DEPENDENCYREQUEST_FILES, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=132, - serialized_end=371, -) - - -_DEPENDENCYRESPONSE_LICENSES = _descriptor.Descriptor( - name='Licenses', - full_name='scanoss.api.dependencies.v2.DependencyResponse.Licenses', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='scanoss.api.dependencies.v2.DependencyResponse.Licenses.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='spdx_id', full_name='scanoss.api.dependencies.v2.DependencyResponse.Licenses.spdx_id', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='is_spdx_approved', full_name='scanoss.api.dependencies.v2.DependencyResponse.Licenses.is_spdx_approved', index=2, - number=3, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='url', full_name='scanoss.api.dependencies.v2.DependencyResponse.Licenses.url', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=521, - serialized_end=601, -) - -_DEPENDENCYRESPONSE_DEPENDENCIES = _descriptor.Descriptor( - name='Dependencies', - full_name='scanoss.api.dependencies.v2.DependencyResponse.Dependencies', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='component', full_name='scanoss.api.dependencies.v2.DependencyResponse.Dependencies.component', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='purl', full_name='scanoss.api.dependencies.v2.DependencyResponse.Dependencies.purl', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='version', full_name='scanoss.api.dependencies.v2.DependencyResponse.Dependencies.version', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='licenses', full_name='scanoss.api.dependencies.v2.DependencyResponse.Dependencies.licenses', index=3, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='url', full_name='scanoss.api.dependencies.v2.DependencyResponse.Dependencies.url', index=4, - number=5, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='comment', full_name='scanoss.api.dependencies.v2.DependencyResponse.Dependencies.comment', index=5, - number=6, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=604, - serialized_end=774, -) - -_DEPENDENCYRESPONSE_FILES = _descriptor.Descriptor( - name='Files', - full_name='scanoss.api.dependencies.v2.DependencyResponse.Files', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='file', full_name='scanoss.api.dependencies.v2.DependencyResponse.Files.file', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='id', full_name='scanoss.api.dependencies.v2.DependencyResponse.Files.id', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='status', full_name='scanoss.api.dependencies.v2.DependencyResponse.Files.status', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='dependencies', full_name='scanoss.api.dependencies.v2.DependencyResponse.Files.dependencies', index=3, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=777, - serialized_end=910, -) - -_DEPENDENCYRESPONSE = _descriptor.Descriptor( - name='DependencyResponse', - full_name='scanoss.api.dependencies.v2.DependencyResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='files', full_name='scanoss.api.dependencies.v2.DependencyResponse.files', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='status', full_name='scanoss.api.dependencies.v2.DependencyResponse.status', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_DEPENDENCYRESPONSE_LICENSES, _DEPENDENCYRESPONSE_DEPENDENCIES, _DEPENDENCYRESPONSE_FILES, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=374, - serialized_end=910, -) - -_DEPENDENCYREQUEST_PURLS.containing_type = _DEPENDENCYREQUEST -_DEPENDENCYREQUEST_FILES.fields_by_name['purls'].message_type = _DEPENDENCYREQUEST_PURLS -_DEPENDENCYREQUEST_FILES.containing_type = _DEPENDENCYREQUEST -_DEPENDENCYREQUEST.fields_by_name['files'].message_type = _DEPENDENCYREQUEST_FILES -_DEPENDENCYRESPONSE_LICENSES.containing_type = _DEPENDENCYRESPONSE -_DEPENDENCYRESPONSE_DEPENDENCIES.fields_by_name['licenses'].message_type = _DEPENDENCYRESPONSE_LICENSES -_DEPENDENCYRESPONSE_DEPENDENCIES.containing_type = _DEPENDENCYRESPONSE -_DEPENDENCYRESPONSE_FILES.fields_by_name['dependencies'].message_type = _DEPENDENCYRESPONSE_DEPENDENCIES -_DEPENDENCYRESPONSE_FILES.containing_type = _DEPENDENCYRESPONSE -_DEPENDENCYRESPONSE.fields_by_name['files'].message_type = _DEPENDENCYRESPONSE_FILES -_DEPENDENCYRESPONSE.fields_by_name['status'].message_type = scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2._STATUSRESPONSE -DESCRIPTOR.message_types_by_name['DependencyRequest'] = _DEPENDENCYREQUEST -DESCRIPTOR.message_types_by_name['DependencyResponse'] = _DEPENDENCYRESPONSE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -DependencyRequest = _reflection.GeneratedProtocolMessageType('DependencyRequest', (_message.Message,), { - - 'Purls' : _reflection.GeneratedProtocolMessageType('Purls', (_message.Message,), { - 'DESCRIPTOR' : _DEPENDENCYREQUEST_PURLS, - '__module__' : 'scanoss.api.dependencies.v2.scanoss_dependencies_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.dependencies.v2.DependencyRequest.Purls) - }) - , - - 'Files' : _reflection.GeneratedProtocolMessageType('Files', (_message.Message,), { - 'DESCRIPTOR' : _DEPENDENCYREQUEST_FILES, - '__module__' : 'scanoss.api.dependencies.v2.scanoss_dependencies_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.dependencies.v2.DependencyRequest.Files) - }) - , - 'DESCRIPTOR' : _DEPENDENCYREQUEST, - '__module__' : 'scanoss.api.dependencies.v2.scanoss_dependencies_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.dependencies.v2.DependencyRequest) - }) -_sym_db.RegisterMessage(DependencyRequest) -_sym_db.RegisterMessage(DependencyRequest.Purls) -_sym_db.RegisterMessage(DependencyRequest.Files) - -DependencyResponse = _reflection.GeneratedProtocolMessageType('DependencyResponse', (_message.Message,), { - - 'Licenses' : _reflection.GeneratedProtocolMessageType('Licenses', (_message.Message,), { - 'DESCRIPTOR' : _DEPENDENCYRESPONSE_LICENSES, - '__module__' : 'scanoss.api.dependencies.v2.scanoss_dependencies_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.dependencies.v2.DependencyResponse.Licenses) - }) - , - - 'Dependencies' : _reflection.GeneratedProtocolMessageType('Dependencies', (_message.Message,), { - 'DESCRIPTOR' : _DEPENDENCYRESPONSE_DEPENDENCIES, - '__module__' : 'scanoss.api.dependencies.v2.scanoss_dependencies_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.dependencies.v2.DependencyResponse.Dependencies) - }) - , - - 'Files' : _reflection.GeneratedProtocolMessageType('Files', (_message.Message,), { - 'DESCRIPTOR' : _DEPENDENCYRESPONSE_FILES, - '__module__' : 'scanoss.api.dependencies.v2.scanoss_dependencies_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.dependencies.v2.DependencyResponse.Files) - }) - , - 'DESCRIPTOR' : _DEPENDENCYRESPONSE, - '__module__' : 'scanoss.api.dependencies.v2.scanoss_dependencies_pb2' - # @@protoc_insertion_point(class_scope:scanoss.api.dependencies.v2.DependencyResponse) - }) -_sym_db.RegisterMessage(DependencyResponse) -_sym_db.RegisterMessage(DependencyResponse.Licenses) -_sym_db.RegisterMessage(DependencyResponse.Dependencies) -_sym_db.RegisterMessage(DependencyResponse.Files) - - -DESCRIPTOR._options = None - -_DEPENDENCIES = _descriptor.ServiceDescriptor( - name='Dependencies', - full_name='scanoss.api.dependencies.v2.Dependencies', - file=DESCRIPTOR, - index=0, - serialized_options=None, - create_key=_descriptor._internal_create_key, - serialized_start=913, - serialized_end=1124, - methods=[ - _descriptor.MethodDescriptor( - name='Echo', - full_name='scanoss.api.dependencies.v2.Dependencies.Echo', - index=0, - containing_service=None, - input_type=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2._ECHOREQUEST, - output_type=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2._ECHORESPONSE, - serialized_options=None, - create_key=_descriptor._internal_create_key, - ), - _descriptor.MethodDescriptor( - name='GetDependencies', - full_name='scanoss.api.dependencies.v2.Dependencies.GetDependencies', - index=1, - containing_service=None, - input_type=_DEPENDENCYREQUEST, - output_type=_DEPENDENCYRESPONSE, - serialized_options=None, - create_key=_descriptor._internal_create_key, - ), -]) -_sym_db.RegisterServiceDescriptor(_DEPENDENCIES) - -DESCRIPTOR.services_by_name['Dependencies'] = _DEPENDENCIES - +from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 +from protoc_gen_openapiv2.options import annotations_pb2 as protoc__gen__openapiv2_dot_options_dot_annotations__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n6scanoss/api/dependencies/v2/scanoss-dependencies.proto\x12\x1bscanoss.api.dependencies.v2\x1a*scanoss/api/common/v2/scanoss-common.proto\x1a\x1cgoogle/api/annotations.proto\x1a.protoc-gen-openapiv2/options/annotations.proto\"\xda\x02\n\x11\x44\x65pendencyRequest\x12\x43\n\x05\x66iles\x18\x01 \x03(\x0b\x32\x34.scanoss.api.dependencies.v2.DependencyRequest.Files\x12\r\n\x05\x64\x65pth\x18\x02 \x01(\x05\x1a*\n\x05Purls\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x13\n\x0brequirement\x18\x02 \x01(\t\x1aZ\n\x05\x46iles\x12\x0c\n\x04\x66ile\x18\x01 \x01(\t\x12\x43\n\x05purls\x18\x02 \x03(\x0b\x32\x34.scanoss.api.dependencies.v2.DependencyRequest.Purls:i\x18\x01\x92\x41\x64\nbJ`{\"files\":[{\"file\":\"package.json\",\"purls\":[{\"purl\":\"pkg:npm/express\",\"requirement\":\"^4.18.0\"}]}]}\"\xd8\x07\n\x12\x44\x65pendencyResponse\x12\x44\n\x05\x66iles\x18\x01 \x03(\x0b\x32\x35.scanoss.api.dependencies.v2.DependencyResponse.Files\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1ak\n\x08Licenses\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x07spdx_id\x18\x02 \x01(\tR\x07spdx_id\x12*\n\x10is_spdx_approved\x18\x03 \x01(\x08R\x10is_spdx_approved\x12\x0b\n\x03url\x18\x04 \x01(\t\x1a\xaa\x01\n\x0c\x44\x65pendencies\x12\x11\n\tcomponent\x18\x01 \x01(\t\x12\x0c\n\x04purl\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12J\n\x08licenses\x18\x04 \x03(\x0b\x32\x38.scanoss.api.dependencies.v2.DependencyResponse.Licenses\x12\x0b\n\x03url\x18\x05 \x01(\t\x12\x0f\n\x07\x63omment\x18\x06 \x01(\t\x1a\x85\x01\n\x05\x46iles\x12\x0c\n\x04\x66ile\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\x12R\n\x0c\x64\x65pendencies\x18\x04 \x03(\x0b\x32<.scanoss.api.dependencies.v2.DependencyResponse.Dependencies:\xa2\x03\x18\x01\x92\x41\x9c\x03\n\x99\x03J\x96\x03{\"files\":[{\"file\":\"package.json\",\"id\":\"dependency\",\"status\":\"pending\",\"dependencies\":[{\"component\":\"express\",\"purl\":\"pkg:npm/express\",\"version\":\"4.18.2\",\"licenses\":[{\"name\":\"MIT\",\"spdx_id\":\"MIT\",\"is_spdx_approved\":true,\"url\":\"https://opensource.org/licenses/MIT\"}],\"url\":\"https://www.npmjs.com/package/express\",\"comment\":\"\"}]}],\"status\":{\"status\":\"SUCCESS\",\"message\":\"Dependencies successfully retrieved\"}}\"\x8d\x02\n\x1bTransitiveDependencyRequest\x12\r\n\x05\x64\x65pth\x18\x01 \x01(\x05\x12\r\n\x05limit\x18\x02 \x01(\x05\x12;\n\ncomponents\x18\x03 \x03(\x0b\x32\'.scanoss.api.common.v2.ComponentRequest:\x92\x01\x92\x41\x8e\x01\n\x8b\x01J\x88\x01{\"depth\":3,\"limit\":50,\"components\":[{\"purl\":\"pkg:npm/express\",\"requirement\":\"4.18.0\"},{\"purl\":\"pkg:npm/lodash\",\"requirement\":\"4.17.0\"}]}\"\x89\x04\n\x1cTransitiveDependencyResponse\x12Y\n\x0c\x64\x65pendencies\x18\x01 \x03(\x0b\x32\x43.scanoss.api.dependencies.v2.TransitiveDependencyResponse.Component\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1a?\n\tComponent\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x13\n\x0brequirement\x18\x03 \x01(\t:\x95\x02\x92\x41\x91\x02\n\x8e\x02J\x8b\x02{\"dependencies\":[{\"purl\":\"pkg:npm/express@4.18.2\",\"version\":\"4.18.2\"},{\"purl\":\"pkg:npm/body-parser@1.20.1\",\"version\":\"1.20.1\"},{\"purl\":\"pkg:npm/cookie@0.5.0\",\"version\":\"0.5.0\"}],\"status\":{\"status\":\"SUCCESS\",\"message\":\"Transitive dependencies successfully retrieved\"}}2\xe9\x03\n\x0c\x44\x65pendencies\x12q\n\x04\x45\x63ho\x12\".scanoss.api.common.v2.EchoRequest\x1a#.scanoss.api.common.v2.EchoResponse\" \x82\xd3\xe4\x93\x02\x1a\"\x15/v2/dependencies/echo:\x01*\x12\x9f\x01\n\x0fGetDependencies\x12..scanoss.api.dependencies.v2.DependencyRequest\x1a/.scanoss.api.dependencies.v2.DependencyResponse\"+\x88\x02\x01\x82\xd3\xe4\x93\x02\"\"\x1d/v2/dependencies/dependencies:\x01*\x12\xc3\x01\n\x19GetTransitiveDependencies\x12\x38.scanoss.api.dependencies.v2.TransitiveDependencyRequest\x1a\x39.scanoss.api.dependencies.v2.TransitiveDependencyResponse\"1\x82\xd3\xe4\x93\x02+\"&/v2/dependencies/transitive/components:\x01*B\x9c\x02Z9github.amrom.workers.dev/scanoss/papi/api/dependenciesv2;dependenciesv2\x92\x41\xdd\x01\x12w\n\x1aSCANOSS Dependency Service\"T\n\x14scanoss-dependencies\x12\'https://github.com/scanoss/dependencies\x1a\x13support@scanoss.com2\x03\x32.0*\x01\x01\x32\x10\x61pplication/json:\x10\x61pplication/jsonR;\n\x03\x34\x30\x34\x12\x34\n*Returned when the resource does not exist.\x12\x06\n\x04\x9a\x02\x01\x07\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'scanoss.api.dependencies.v2.scanoss_dependencies_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z9github.amrom.workers.dev/scanoss/papi/api/dependenciesv2;dependenciesv2\222A\335\001\022w\n\032SCANOSS Dependency Service\"T\n\024scanoss-dependencies\022\'https://github.com/scanoss/dependencies\032\023support@scanoss.com2\0032.0*\001\0012\020application/json:\020application/jsonR;\n\003404\0224\n*Returned when the resource does not exist.\022\006\n\004\232\002\001\007' + _globals['_DEPENDENCYREQUEST']._loaded_options = None + _globals['_DEPENDENCYREQUEST']._serialized_options = b'\030\001\222Ad\nbJ`{\"files\":[{\"file\":\"package.json\",\"purls\":[{\"purl\":\"pkg:npm/express\",\"requirement\":\"^4.18.0\"}]}]}' + _globals['_DEPENDENCYRESPONSE']._loaded_options = None + _globals['_DEPENDENCYRESPONSE']._serialized_options = b'\030\001\222A\234\003\n\231\003J\226\003{\"files\":[{\"file\":\"package.json\",\"id\":\"dependency\",\"status\":\"pending\",\"dependencies\":[{\"component\":\"express\",\"purl\":\"pkg:npm/express\",\"version\":\"4.18.2\",\"licenses\":[{\"name\":\"MIT\",\"spdx_id\":\"MIT\",\"is_spdx_approved\":true,\"url\":\"https://opensource.org/licenses/MIT\"}],\"url\":\"https://www.npmjs.com/package/express\",\"comment\":\"\"}]}],\"status\":{\"status\":\"SUCCESS\",\"message\":\"Dependencies successfully retrieved\"}}' + _globals['_TRANSITIVEDEPENDENCYREQUEST']._loaded_options = None + _globals['_TRANSITIVEDEPENDENCYREQUEST']._serialized_options = b'\222A\216\001\n\213\001J\210\001{\"depth\":3,\"limit\":50,\"components\":[{\"purl\":\"pkg:npm/express\",\"requirement\":\"4.18.0\"},{\"purl\":\"pkg:npm/lodash\",\"requirement\":\"4.17.0\"}]}' + _globals['_TRANSITIVEDEPENDENCYRESPONSE']._loaded_options = None + _globals['_TRANSITIVEDEPENDENCYRESPONSE']._serialized_options = b'\222A\221\002\n\216\002J\213\002{\"dependencies\":[{\"purl\":\"pkg:npm/express@4.18.2\",\"version\":\"4.18.2\"},{\"purl\":\"pkg:npm/body-parser@1.20.1\",\"version\":\"1.20.1\"},{\"purl\":\"pkg:npm/cookie@0.5.0\",\"version\":\"0.5.0\"}],\"status\":{\"status\":\"SUCCESS\",\"message\":\"Transitive dependencies successfully retrieved\"}}' + _globals['_DEPENDENCIES'].methods_by_name['Echo']._loaded_options = None + _globals['_DEPENDENCIES'].methods_by_name['Echo']._serialized_options = b'\202\323\344\223\002\032\"\025/v2/dependencies/echo:\001*' + _globals['_DEPENDENCIES'].methods_by_name['GetDependencies']._loaded_options = None + _globals['_DEPENDENCIES'].methods_by_name['GetDependencies']._serialized_options = b'\210\002\001\202\323\344\223\002\"\"\035/v2/dependencies/dependencies:\001*' + _globals['_DEPENDENCIES'].methods_by_name['GetTransitiveDependencies']._loaded_options = None + _globals['_DEPENDENCIES'].methods_by_name['GetTransitiveDependencies']._serialized_options = b'\202\323\344\223\002+\"&/v2/dependencies/transitive/components:\001*' + _globals['_DEPENDENCYREQUEST']._serialized_start=210 + _globals['_DEPENDENCYREQUEST']._serialized_end=556 + _globals['_DEPENDENCYREQUEST_PURLS']._serialized_start=315 + _globals['_DEPENDENCYREQUEST_PURLS']._serialized_end=357 + _globals['_DEPENDENCYREQUEST_FILES']._serialized_start=359 + _globals['_DEPENDENCYREQUEST_FILES']._serialized_end=449 + _globals['_DEPENDENCYRESPONSE']._serialized_start=559 + _globals['_DEPENDENCYRESPONSE']._serialized_end=1543 + _globals['_DEPENDENCYRESPONSE_LICENSES']._serialized_start=706 + _globals['_DEPENDENCYRESPONSE_LICENSES']._serialized_end=813 + _globals['_DEPENDENCYRESPONSE_DEPENDENCIES']._serialized_start=816 + _globals['_DEPENDENCYRESPONSE_DEPENDENCIES']._serialized_end=986 + _globals['_DEPENDENCYRESPONSE_FILES']._serialized_start=989 + _globals['_DEPENDENCYRESPONSE_FILES']._serialized_end=1122 + _globals['_TRANSITIVEDEPENDENCYREQUEST']._serialized_start=1546 + _globals['_TRANSITIVEDEPENDENCYREQUEST']._serialized_end=1815 + _globals['_TRANSITIVEDEPENDENCYRESPONSE']._serialized_start=1818 + _globals['_TRANSITIVEDEPENDENCYRESPONSE']._serialized_end=2339 + _globals['_TRANSITIVEDEPENDENCYRESPONSE_COMPONENT']._serialized_start=1996 + _globals['_TRANSITIVEDEPENDENCYRESPONSE_COMPONENT']._serialized_end=2059 + _globals['_DEPENDENCIES']._serialized_start=2342 + _globals['_DEPENDENCIES']._serialized_end=2831 # @@protoc_insertion_point(module_scope) diff --git a/src/scanoss/api/dependencies/v2/scanoss_dependencies_pb2_grpc.py b/src/scanoss/api/dependencies/v2/scanoss_dependencies_pb2_grpc.py index cddf7cfa..7e8f6ab3 100644 --- a/src/scanoss/api/dependencies/v2/scanoss_dependencies_pb2_grpc.py +++ b/src/scanoss/api/dependencies/v2/scanoss_dependencies_pb2_grpc.py @@ -1,10 +1,30 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc +import warnings from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 from scanoss.api.dependencies.v2 import scanoss_dependencies_pb2 as scanoss_dot_api_dot_dependencies_dot_v2_dot_scanoss__dependencies__pb2 +GRPC_GENERATED_VERSION = '1.73.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in scanoss/api/dependencies/v2/scanoss_dependencies_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + class DependenciesStub(object): """ @@ -21,12 +41,17 @@ def __init__(self, channel): '/scanoss.api.dependencies.v2.Dependencies/Echo', request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, response_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, - ) + _registered_method=True) self.GetDependencies = channel.unary_unary( '/scanoss.api.dependencies.v2.Dependencies/GetDependencies', request_serializer=scanoss_dot_api_dot_dependencies_dot_v2_dot_scanoss__dependencies__pb2.DependencyRequest.SerializeToString, response_deserializer=scanoss_dot_api_dot_dependencies_dot_v2_dot_scanoss__dependencies__pb2.DependencyResponse.FromString, - ) + _registered_method=True) + self.GetTransitiveDependencies = channel.unary_unary( + '/scanoss.api.dependencies.v2.Dependencies/GetTransitiveDependencies', + request_serializer=scanoss_dot_api_dot_dependencies_dot_v2_dot_scanoss__dependencies__pb2.TransitiveDependencyRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_dependencies_dot_v2_dot_scanoss__dependencies__pb2.TransitiveDependencyResponse.FromString, + _registered_method=True) class DependenciesServicer(object): @@ -43,6 +68,14 @@ def Echo(self, request, context): def GetDependencies(self, request, context): """Get dependency details + Deprecated: Use /v2/licenses/components instead + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetTransitiveDependencies(self, request, context): + """Get transitive dependency details """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') @@ -61,10 +94,16 @@ def add_DependenciesServicer_to_server(servicer, server): request_deserializer=scanoss_dot_api_dot_dependencies_dot_v2_dot_scanoss__dependencies__pb2.DependencyRequest.FromString, response_serializer=scanoss_dot_api_dot_dependencies_dot_v2_dot_scanoss__dependencies__pb2.DependencyResponse.SerializeToString, ), + 'GetTransitiveDependencies': grpc.unary_unary_rpc_method_handler( + servicer.GetTransitiveDependencies, + request_deserializer=scanoss_dot_api_dot_dependencies_dot_v2_dot_scanoss__dependencies__pb2.TransitiveDependencyRequest.FromString, + response_serializer=scanoss_dot_api_dot_dependencies_dot_v2_dot_scanoss__dependencies__pb2.TransitiveDependencyResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'scanoss.api.dependencies.v2.Dependencies', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('scanoss.api.dependencies.v2.Dependencies', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -84,11 +123,21 @@ def Echo(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/scanoss.api.dependencies.v2.Dependencies/Echo', + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.dependencies.v2.Dependencies/Echo', scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def GetDependencies(request, @@ -101,8 +150,45 @@ def GetDependencies(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/scanoss.api.dependencies.v2.Dependencies/GetDependencies', + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.dependencies.v2.Dependencies/GetDependencies', scanoss_dot_api_dot_dependencies_dot_v2_dot_scanoss__dependencies__pb2.DependencyRequest.SerializeToString, scanoss_dot_api_dot_dependencies_dot_v2_dot_scanoss__dependencies__pb2.DependencyResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetTransitiveDependencies(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.dependencies.v2.Dependencies/GetTransitiveDependencies', + scanoss_dot_api_dot_dependencies_dot_v2_dot_scanoss__dependencies__pb2.TransitiveDependencyRequest.SerializeToString, + scanoss_dot_api_dot_dependencies_dot_v2_dot_scanoss__dependencies__pb2.TransitiveDependencyResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/tests/winnowing-test.py b/src/scanoss/api/geoprovenance/__init__.py similarity index 51% rename from tests/winnowing-test.py rename to src/scanoss/api/geoprovenance/__init__.py index 56dc4f4e..d2ed05b0 100644 --- a/tests/winnowing-test.py +++ b/src/scanoss/api/geoprovenance/__init__.py @@ -21,37 +21,3 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -import unittest - -from scanoss.winnowing import Winnowing - - -class MyTestCase(unittest.TestCase): - """ - Exercise the Winnowing class - """ - def test_winnowing(self): - winnowing = Winnowing(debug=True) - filename = "test-file.c" - contents = "c code contents" - content_types = bytes(contents, encoding="raw_unicode_escape") - wfp = winnowing.wfp_for_contents(filename, False, content_types) - print(f'WFP for {filename}: {wfp}') - self.assertIsNotNone(wfp) - filename = __file__ - wfp = winnowing.wfp_for_file(filename, filename) - print(f'WFP for {filename}: {wfp}') - self.assertIsNotNone(wfp) - - def test_snippet_skip(self): - winnowing = Winnowing(debug=True) - filename = "test-file.jar" - contents = "jar file contents" - content_types = bytes(contents, encoding="raw_unicode_escape") - wfp = winnowing.wfp_for_contents(filename, False, content_types) - print(f'WFP for {filename}: {wfp}') - self.assertIsNotNone(wfp) - - -if __name__ == '__main__': - unittest.main() diff --git a/src/scanoss/api/geoprovenance/v2/__init__.py b/src/scanoss/api/geoprovenance/v2/__init__.py new file mode 100644 index 00000000..d2ed05b0 --- /dev/null +++ b/src/scanoss/api/geoprovenance/v2/__init__.py @@ -0,0 +1,23 @@ +""" + SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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/src/scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2.py b/src/scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2.py new file mode 100644 index 00000000..148ba0fc --- /dev/null +++ b/src/scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: scanoss/api/geoprovenance/v2/scanoss-geoprovenance.proto +# Protobuf Python Version: 6.31.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 0, + '', + 'scanoss/api/geoprovenance/v2/scanoss-geoprovenance.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 +from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 +from protoc_gen_openapiv2.options import annotations_pb2 as protoc__gen__openapiv2_dot_options_dot_annotations__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n8scanoss/api/geoprovenance/v2/scanoss-geoprovenance.proto\x12\x1cscanoss.api.geoprovenance.v2\x1a*scanoss/api/common/v2/scanoss-common.proto\x1a\x1cgoogle/api/annotations.proto\x1a.protoc-gen-openapiv2/options/annotations.proto\"2\n\x10\x44\x65\x63laredLocation\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x10\n\x08location\x18\x02 \x01(\t\"1\n\x0f\x43uratedLocation\x12\x0f\n\x07\x63ountry\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x05\"\xed\x02\n\x13\x43ontributorResponse\x12\x46\n\x05purls\x18\x01 \x03(\x0b\x32\x37.scanoss.api.geoprovenance.v2.ContributorResponse.Purls\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1a\xd2\x01\n\x05Purls\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12^\n\x12\x64\x65\x63lared_locations\x18\x02 \x03(\x0b\x32..scanoss.api.geoprovenance.v2.DeclaredLocationR\x12\x64\x65\x63lared_locations\x12[\n\x11\x63urated_locations\x18\x03 \x03(\x0b\x32-.scanoss.api.geoprovenance.v2.CuratedLocationR\x11\x63urated_locations:\x02\x18\x01\"\xe2\x01\n\x15\x43omponentLocationInfo\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12^\n\x12\x64\x65\x63lared_locations\x18\x02 \x03(\x0b\x32..scanoss.api.geoprovenance.v2.DeclaredLocationR\x12\x64\x65\x63lared_locations\x12[\n\x11\x63urated_locations\x18\x03 \x03(\x0b\x32-.scanoss.api.geoprovenance.v2.CuratedLocationR\x11\x63urated_locations\"\xd5\x04\n\x1d\x43omponentsContributorResponse\x12g\n\x14\x63omponents_locations\x18\x01 \x03(\x0b\x32\x33.scanoss.api.geoprovenance.v2.ComponentLocationInfoR\x14\x63omponents_locations\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse:\x93\x03\x92\x41\x8f\x03\n\x8c\x03J\x89\x03{\"components_locations\":[{\"purl\":\"pkg:github/scanoss/engine@5.0.0\",\"declared_locations\":[{\"type\":\"owner\",\"location\":\"Barcelona, Spain\"},{\"type\":\"contributor\",\"location\":\"Berlin, Germany\"}],\"curated_locations\":[{\"country\":\"Spain\",\"count\":8},{\"country\":\"Germany\",\"count\":3},{\"country\":\"United States\",\"count\":2}]}],\"status\":{\"status\":\"SUCCESS\",\"message\":\"Geo-provenance successfully retrieved\"}}\"\xce\x04\n\x1c\x43omponentContributorResponse\x12\x65\n\x13\x63omponent_locations\x18\x01 \x01(\x0b\x32\x33.scanoss.api.geoprovenance.v2.ComponentLocationInfoR\x13\x63omponent_locations\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse:\x8f\x03\x92\x41\x8b\x03\n\x88\x03J\x85\x03{\"component_location\":{\"purl\":\"pkg:github/scanoss/engine@5.0.0\",\"declared_locations\":[{\"type\":\"owner\",\"location\":\"Barcelona, Spain\"},{\"type\":\"contributor\",\"location\":\"Berlin, Germany\"}],\"curated_locations\":[{\"country\":\"Spain\",\"count\":8},{\"country\":\"Germany\",\"count\":3},{\"country\":\"United States\",\"count\":2}]},\"status\":{\"status\":\"SUCCESS\",\"message\":\"Geo-provenance successfully retrieved\"}}\",\n\x08Location\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\npercentage\x18\x02 \x01(\x02\"\\\n\x11\x43omponentLocation\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x39\n\tlocations\x18\x02 \x03(\x0b\x32&.scanoss.api.geoprovenance.v2.Location\"\xe0\x01\n\x0eOriginResponse\x12\x41\n\x05purls\x18\x01 \x03(\x0b\x32\x32.scanoss.api.geoprovenance.v2.OriginResponse.Purls\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1aP\n\x05Purls\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x39\n\tlocations\x18\x02 \x03(\x0b\x32&.scanoss.api.geoprovenance.v2.Location:\x02\x18\x01\"\xcd\x03\n\x18\x43omponentsOriginResponse\x12\x63\n\x14\x63omponents_locations\x18\x01 \x03(\x0b\x32/.scanoss.api.geoprovenance.v2.ComponentLocationR\x14\x63omponents_locations\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse:\x94\x02\x92\x41\x90\x02\n\x8d\x02J\x8a\x02{\"components_locations\":[{\"purl\":\"pkg:github/scanoss/engine@5.0.0\",\"locations\":[{\"name\":\"ES\",\"percentage\":65.5},{\"name\":\"DE\",\"percentage\":20.3},{\"name\":\"US\",\"percentage\":14.2}]}],\"status\":{\"status\":\"SUCCESS\",\"message\":\"Geo-provenance origin successfully retrieved\"}}\"\xc8\x03\n\x17\x43omponentOriginResponse\x12\x61\n\x13\x63omponent_locations\x18\x01 \x01(\x0b\x32/.scanoss.api.geoprovenance.v2.ComponentLocationR\x13\x63omponent_locations\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse:\x92\x02\x92\x41\x8e\x02\n\x8b\x02J\x88\x02{\"component_locations\": {\"purl\":\"pkg:github/scanoss/engine@5.0.0\",\"locations\":[{\"name\":\"ES\",\"percentage\":65.5},{\"name\":\"DE\",\"percentage\":20.3},{\"name\":\"US\",\"percentage\":14.2}]},\"status\":{\"status\":\"SUCCESS\",\"message\":\"Geo-provenance origin successfully retrieved\"}}2\xff\x08\n\rGeoProvenance\x12r\n\x04\x45\x63ho\x12\".scanoss.api.common.v2.EchoRequest\x1a#.scanoss.api.common.v2.EchoResponse\"!\x82\xd3\xe4\x93\x02\x1b\"\x16/v2/geoprovenance/echo:\x01*\x12\x9c\x01\n\x18GetComponentContributors\x12\".scanoss.api.common.v2.PurlRequest\x1a\x31.scanoss.api.geoprovenance.v2.ContributorResponse\")\x88\x02\x01\x82\xd3\xe4\x93\x02 \"\x1b/v2/geoprovenance/countries:\x01*\x12\xbe\x01\n\"GetCountryContributorsByComponents\x12(.scanoss.api.common.v2.ComponentsRequest\x1a;.scanoss.api.geoprovenance.v2.ComponentsContributorResponse\"1\x82\xd3\xe4\x93\x02+\"&/v2/geoprovenance/countries/components:\x01*\x12\xb7\x01\n!GetCountryContributorsByComponent\x12\'.scanoss.api.common.v2.ComponentRequest\x1a:.scanoss.api.geoprovenance.v2.ComponentContributorResponse\"-\x82\xd3\xe4\x93\x02\'\x12%/v2/geoprovenance/countries/component\x12\x8e\x01\n\x12GetComponentOrigin\x12\".scanoss.api.common.v2.PurlRequest\x1a,.scanoss.api.geoprovenance.v2.OriginResponse\"&\x88\x02\x01\x82\xd3\xe4\x93\x02\x1d\"\x18/v2/geoprovenance/origin:\x01*\x12\xa9\x01\n\x15GetOriginByComponents\x12(.scanoss.api.common.v2.ComponentsRequest\x1a\x36.scanoss.api.geoprovenance.v2.ComponentsOriginResponse\".\x82\xd3\xe4\x93\x02(\"#/v2/geoprovenance/origin/components:\x01*\x12\xa2\x01\n\x14GetOriginByComponent\x12\'.scanoss.api.common.v2.ComponentRequest\x1a\x35.scanoss.api.geoprovenance.v2.ComponentOriginResponse\"*\x82\xd3\xe4\x93\x02$\x12\"/v2/geoprovenance/origin/componentB\xa4\x02Z;github.com/scanoss/papi/api/geoprovenancev2;geoprovenancev2\x92\x41\xe3\x01\x12}\n\x1eSCANOSS GEO Provenance Service\"V\n\x15scanoss-geoprovenance\x12(https://github.com/scanoss/geoprovenance\x1a\x13support@scanoss.com2\x03\x32.0*\x01\x01\x32\x10\x61pplication/json:\x10\x61pplication/jsonR;\n\x03\x34\x30\x34\x12\x34\n*Returned when the resource does not exist.\x12\x06\n\x04\x9a\x02\x01\x07\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'scanoss.api.geoprovenance.v2.scanoss_geoprovenance_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z;github.com/scanoss/papi/api/geoprovenancev2;geoprovenancev2\222A\343\001\022}\n\036SCANOSS GEO Provenance Service\"V\n\025scanoss-geoprovenance\022(https://github.com/scanoss/geoprovenance\032\023support@scanoss.com2\0032.0*\001\0012\020application/json:\020application/jsonR;\n\003404\0224\n*Returned when the resource does not exist.\022\006\n\004\232\002\001\007' + _globals['_CONTRIBUTORRESPONSE']._loaded_options = None + _globals['_CONTRIBUTORRESPONSE']._serialized_options = b'\030\001' + _globals['_COMPONENTSCONTRIBUTORRESPONSE']._loaded_options = None + _globals['_COMPONENTSCONTRIBUTORRESPONSE']._serialized_options = b'\222A\217\003\n\214\003J\211\003{\"components_locations\":[{\"purl\":\"pkg:github/scanoss/engine@5.0.0\",\"declared_locations\":[{\"type\":\"owner\",\"location\":\"Barcelona, Spain\"},{\"type\":\"contributor\",\"location\":\"Berlin, Germany\"}],\"curated_locations\":[{\"country\":\"Spain\",\"count\":8},{\"country\":\"Germany\",\"count\":3},{\"country\":\"United States\",\"count\":2}]}],\"status\":{\"status\":\"SUCCESS\",\"message\":\"Geo-provenance successfully retrieved\"}}' + _globals['_COMPONENTCONTRIBUTORRESPONSE']._loaded_options = None + _globals['_COMPONENTCONTRIBUTORRESPONSE']._serialized_options = b'\222A\213\003\n\210\003J\205\003{\"component_location\":{\"purl\":\"pkg:github/scanoss/engine@5.0.0\",\"declared_locations\":[{\"type\":\"owner\",\"location\":\"Barcelona, Spain\"},{\"type\":\"contributor\",\"location\":\"Berlin, Germany\"}],\"curated_locations\":[{\"country\":\"Spain\",\"count\":8},{\"country\":\"Germany\",\"count\":3},{\"country\":\"United States\",\"count\":2}]},\"status\":{\"status\":\"SUCCESS\",\"message\":\"Geo-provenance successfully retrieved\"}}' + _globals['_ORIGINRESPONSE']._loaded_options = None + _globals['_ORIGINRESPONSE']._serialized_options = b'\030\001' + _globals['_COMPONENTSORIGINRESPONSE']._loaded_options = None + _globals['_COMPONENTSORIGINRESPONSE']._serialized_options = b'\222A\220\002\n\215\002J\212\002{\"components_locations\":[{\"purl\":\"pkg:github/scanoss/engine@5.0.0\",\"locations\":[{\"name\":\"ES\",\"percentage\":65.5},{\"name\":\"DE\",\"percentage\":20.3},{\"name\":\"US\",\"percentage\":14.2}]}],\"status\":{\"status\":\"SUCCESS\",\"message\":\"Geo-provenance origin successfully retrieved\"}}' + _globals['_COMPONENTORIGINRESPONSE']._loaded_options = None + _globals['_COMPONENTORIGINRESPONSE']._serialized_options = b'\222A\216\002\n\213\002J\210\002{\"component_locations\": {\"purl\":\"pkg:github/scanoss/engine@5.0.0\",\"locations\":[{\"name\":\"ES\",\"percentage\":65.5},{\"name\":\"DE\",\"percentage\":20.3},{\"name\":\"US\",\"percentage\":14.2}]},\"status\":{\"status\":\"SUCCESS\",\"message\":\"Geo-provenance origin successfully retrieved\"}}' + _globals['_GEOPROVENANCE'].methods_by_name['Echo']._loaded_options = None + _globals['_GEOPROVENANCE'].methods_by_name['Echo']._serialized_options = b'\202\323\344\223\002\033\"\026/v2/geoprovenance/echo:\001*' + _globals['_GEOPROVENANCE'].methods_by_name['GetComponentContributors']._loaded_options = None + _globals['_GEOPROVENANCE'].methods_by_name['GetComponentContributors']._serialized_options = b'\210\002\001\202\323\344\223\002 \"\033/v2/geoprovenance/countries:\001*' + _globals['_GEOPROVENANCE'].methods_by_name['GetCountryContributorsByComponents']._loaded_options = None + _globals['_GEOPROVENANCE'].methods_by_name['GetCountryContributorsByComponents']._serialized_options = b'\202\323\344\223\002+\"&/v2/geoprovenance/countries/components:\001*' + _globals['_GEOPROVENANCE'].methods_by_name['GetCountryContributorsByComponent']._loaded_options = None + _globals['_GEOPROVENANCE'].methods_by_name['GetCountryContributorsByComponent']._serialized_options = b'\202\323\344\223\002\'\022%/v2/geoprovenance/countries/component' + _globals['_GEOPROVENANCE'].methods_by_name['GetComponentOrigin']._loaded_options = None + _globals['_GEOPROVENANCE'].methods_by_name['GetComponentOrigin']._serialized_options = b'\210\002\001\202\323\344\223\002\035\"\030/v2/geoprovenance/origin:\001*' + _globals['_GEOPROVENANCE'].methods_by_name['GetOriginByComponents']._loaded_options = None + _globals['_GEOPROVENANCE'].methods_by_name['GetOriginByComponents']._serialized_options = b'\202\323\344\223\002(\"#/v2/geoprovenance/origin/components:\001*' + _globals['_GEOPROVENANCE'].methods_by_name['GetOriginByComponent']._loaded_options = None + _globals['_GEOPROVENANCE'].methods_by_name['GetOriginByComponent']._serialized_options = b'\202\323\344\223\002$\022\"/v2/geoprovenance/origin/component' + _globals['_DECLAREDLOCATION']._serialized_start=212 + _globals['_DECLAREDLOCATION']._serialized_end=262 + _globals['_CURATEDLOCATION']._serialized_start=264 + _globals['_CURATEDLOCATION']._serialized_end=313 + _globals['_CONTRIBUTORRESPONSE']._serialized_start=316 + _globals['_CONTRIBUTORRESPONSE']._serialized_end=681 + _globals['_CONTRIBUTORRESPONSE_PURLS']._serialized_start=467 + _globals['_CONTRIBUTORRESPONSE_PURLS']._serialized_end=677 + _globals['_COMPONENTLOCATIONINFO']._serialized_start=684 + _globals['_COMPONENTLOCATIONINFO']._serialized_end=910 + _globals['_COMPONENTSCONTRIBUTORRESPONSE']._serialized_start=913 + _globals['_COMPONENTSCONTRIBUTORRESPONSE']._serialized_end=1510 + _globals['_COMPONENTCONTRIBUTORRESPONSE']._serialized_start=1513 + _globals['_COMPONENTCONTRIBUTORRESPONSE']._serialized_end=2103 + _globals['_LOCATION']._serialized_start=2105 + _globals['_LOCATION']._serialized_end=2149 + _globals['_COMPONENTLOCATION']._serialized_start=2151 + _globals['_COMPONENTLOCATION']._serialized_end=2243 + _globals['_ORIGINRESPONSE']._serialized_start=2246 + _globals['_ORIGINRESPONSE']._serialized_end=2470 + _globals['_ORIGINRESPONSE_PURLS']._serialized_start=2386 + _globals['_ORIGINRESPONSE_PURLS']._serialized_end=2466 + _globals['_COMPONENTSORIGINRESPONSE']._serialized_start=2473 + _globals['_COMPONENTSORIGINRESPONSE']._serialized_end=2934 + _globals['_COMPONENTORIGINRESPONSE']._serialized_start=2937 + _globals['_COMPONENTORIGINRESPONSE']._serialized_end=3393 + _globals['_GEOPROVENANCE']._serialized_start=3396 + _globals['_GEOPROVENANCE']._serialized_end=4547 +# @@protoc_insertion_point(module_scope) diff --git a/src/scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2_grpc.py b/src/scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2_grpc.py new file mode 100644 index 00000000..5ab07c9a --- /dev/null +++ b/src/scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2_grpc.py @@ -0,0 +1,381 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 +from scanoss.api.geoprovenance.v2 import scanoss_geoprovenance_pb2 as scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2 + +GRPC_GENERATED_VERSION = '1.73.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class GeoProvenanceStub(object): + """* + Expose all of the SCANOSS Geo Provenance RPCs here + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Echo = channel.unary_unary( + '/scanoss.api.geoprovenance.v2.GeoProvenance/Echo', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, + _registered_method=True) + self.GetComponentContributors = channel.unary_unary( + '/scanoss.api.geoprovenance.v2.GeoProvenance/GetComponentContributors', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.ContributorResponse.FromString, + _registered_method=True) + self.GetCountryContributorsByComponents = channel.unary_unary( + '/scanoss.api.geoprovenance.v2.GeoProvenance/GetCountryContributorsByComponents', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.ComponentsContributorResponse.FromString, + _registered_method=True) + self.GetCountryContributorsByComponent = channel.unary_unary( + '/scanoss.api.geoprovenance.v2.GeoProvenance/GetCountryContributorsByComponent', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.ComponentContributorResponse.FromString, + _registered_method=True) + self.GetComponentOrigin = channel.unary_unary( + '/scanoss.api.geoprovenance.v2.GeoProvenance/GetComponentOrigin', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.OriginResponse.FromString, + _registered_method=True) + self.GetOriginByComponents = channel.unary_unary( + '/scanoss.api.geoprovenance.v2.GeoProvenance/GetOriginByComponents', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.ComponentsOriginResponse.FromString, + _registered_method=True) + self.GetOriginByComponent = channel.unary_unary( + '/scanoss.api.geoprovenance.v2.GeoProvenance/GetOriginByComponent', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.ComponentOriginResponse.FromString, + _registered_method=True) + + +class GeoProvenanceServicer(object): + """* + Expose all of the SCANOSS Geo Provenance RPCs here + """ + + def Echo(self, request, context): + """Standard health check endpoint to verify service availability and connectivity + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentContributors(self, request, context): + """[DEPRECATED] Get component-level Geo Provenance based on contributor declared location + This method accepts PURL-based requests and is deprecated in favor of GetCountryContributorsByComponent + which accepts ComponentRequest for better component identification + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetCountryContributorsByComponents(self, request, context): + """Get component-level Geo Provenance based on contributor declared location + This is the current method that accepts ComponentsRequest for enhanced component identification + Replaces the deprecated GetComponentContributors method + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetCountryContributorsByComponent(self, request, context): + """Get component-level Geo Provenance based on contributor declared location + This is the current method that accepts ComponentRequest for enhanced component identification + Replaces the deprecated GetComponentContributors method + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentOrigin(self, request, context): + """[DEPRECATED] Get component-level Geo Provenance based on contributor origin commit times + This method accepts PURL-based requests and is deprecated in favor of GetOriginByComponent + which accepts ComponentRequest for better component identification + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetOriginByComponents(self, request, context): + """Get component-level Geo Provenance based on contributor origin commit times + This is the current method that accepts ComponentsRequest for enhanced component identification + Replaces the deprecated GetComponentOrigin method + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetOriginByComponent(self, request, context): + """Get component-level Geo Provenance based on contributor origin commit times + This is the current method that accepts ComponentRequest for enhanced component identification + Replaces the deprecated GetComponentOrigin method + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_GeoProvenanceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Echo': grpc.unary_unary_rpc_method_handler( + servicer.Echo, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.FromString, + response_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.SerializeToString, + ), + 'GetComponentContributors': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentContributors, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.FromString, + response_serializer=scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.ContributorResponse.SerializeToString, + ), + 'GetCountryContributorsByComponents': grpc.unary_unary_rpc_method_handler( + servicer.GetCountryContributorsByComponents, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.FromString, + response_serializer=scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.ComponentsContributorResponse.SerializeToString, + ), + 'GetCountryContributorsByComponent': grpc.unary_unary_rpc_method_handler( + servicer.GetCountryContributorsByComponent, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.FromString, + response_serializer=scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.ComponentContributorResponse.SerializeToString, + ), + 'GetComponentOrigin': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentOrigin, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.FromString, + response_serializer=scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.OriginResponse.SerializeToString, + ), + 'GetOriginByComponents': grpc.unary_unary_rpc_method_handler( + servicer.GetOriginByComponents, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.FromString, + response_serializer=scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.ComponentsOriginResponse.SerializeToString, + ), + 'GetOriginByComponent': grpc.unary_unary_rpc_method_handler( + servicer.GetOriginByComponent, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.FromString, + response_serializer=scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.ComponentOriginResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'scanoss.api.geoprovenance.v2.GeoProvenance', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('scanoss.api.geoprovenance.v2.GeoProvenance', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class GeoProvenance(object): + """* + Expose all of the SCANOSS Geo Provenance RPCs here + """ + + @staticmethod + def Echo(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.geoprovenance.v2.GeoProvenance/Echo', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentContributors(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.geoprovenance.v2.GeoProvenance/GetComponentContributors', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.ContributorResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetCountryContributorsByComponents(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.geoprovenance.v2.GeoProvenance/GetCountryContributorsByComponents', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.ComponentsContributorResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetCountryContributorsByComponent(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.geoprovenance.v2.GeoProvenance/GetCountryContributorsByComponent', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.ComponentContributorResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentOrigin(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.geoprovenance.v2.GeoProvenance/GetComponentOrigin', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.OriginResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetOriginByComponents(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.geoprovenance.v2.GeoProvenance/GetOriginByComponents', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.ComponentsOriginResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetOriginByComponent(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.geoprovenance.v2.GeoProvenance/GetOriginByComponent', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + scanoss_dot_api_dot_geoprovenance_dot_v2_dot_scanoss__geoprovenance__pb2.ComponentOriginResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/src/scanoss/api/licenses/__init__.py b/src/scanoss/api/licenses/__init__.py new file mode 100644 index 00000000..1e95c46d --- /dev/null +++ b/src/scanoss/api/licenses/__init__.py @@ -0,0 +1,23 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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/src/scanoss/api/licenses/v2/__init__.py b/src/scanoss/api/licenses/v2/__init__.py new file mode 100644 index 00000000..1e95c46d --- /dev/null +++ b/src/scanoss/api/licenses/v2/__init__.py @@ -0,0 +1,23 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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/src/scanoss/api/licenses/v2/scanoss_licenses_pb2.py b/src/scanoss/api/licenses/v2/scanoss_licenses_pb2.py new file mode 100644 index 00000000..1476d0e9 --- /dev/null +++ b/src/scanoss/api/licenses/v2/scanoss_licenses_pb2.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: scanoss/api/licenses/v2/scanoss-licenses.proto +# Protobuf Python Version: 6.31.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 0, + '', + 'scanoss/api/licenses/v2/scanoss-licenses.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 +from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 +from protoc_gen_openapiv2.options import annotations_pb2 as protoc__gen__openapiv2_dot_options_dot_annotations__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.scanoss/api/licenses/v2/scanoss-licenses.proto\x12\x17scanoss.api.licenses.v2\x1a*scanoss/api/common/v2/scanoss-common.proto\x1a\x1cgoogle/api/annotations.proto\x1a.protoc-gen-openapiv2/options/annotations.proto\"\xbd\x03\n\x18\x43omponentLicenseResponse\x12@\n\tcomponent\x18\x01 \x01(\x0b\x32-.scanoss.api.licenses.v2.ComponentLicenseInfo\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse:\xa7\x02\x92\x41\xa3\x02\n\xa0\x02J\x9d\x02{\"component\":{\"purl\": \"pkg:github/scanoss/engine@1.0.0\", \"requirement\": \"\", \"version\": \"1.0.0\", \"statement\": \"GPL-2.0\", \"licenses\": [{\"id\": \"GPL-2.0\", \"full_name\": \"GNU General Public License v2.0 only\"}]}, \"status\": {\"status\": \"SUCCESS\", \"message\": \"Licenses Successfully retrieved\"}}\"\xe9\x04\n\x19\x43omponentsLicenseResponse\x12\x41\n\ncomponents\x18\x01 \x03(\x0b\x32-.scanoss.api.licenses.v2.ComponentLicenseInfo\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse:\xd1\x03\x92\x41\xcd\x03\n\xca\x03J\xc7\x03{\"components\":[{\"purl\": \"pkg:github/scanoss/engine@1.0.0\", \"requirement\": \"\", \"version\": \"1.0.0\", \"statement\": \"GPL-2.0\", \"licenses\": [{\"id\": \"GPL-2.0\", \"full_name\": \"GNU General Public License v2.0 only\"}]}, {\"purl\": \"pkg:github/scanoss/scanoss.py@v1.30.0\",\"requirement\": \"\",\"version\": \"v1.30.0\",\"statement\": \"MIT\", \"licenses\": [{\"id\": \"MIT\",\"full_name\": \"MIT License\"}]} ], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Licenses Successfully retrieved\"}}\"\x89\x01\n\x16LicenseDetailsResponse\x12\x38\n\x07license\x18\x01 \x01(\x0b\x32\'.scanoss.api.licenses.v2.LicenseDetails\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\"\x81\x01\n\x13ObligationsResponse\x12\x33\n\x0bobligations\x18\x01 \x01(\x0b\x32\x1e.scanoss.api.licenses.v2.OSADL\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\"\xe4\x05\n\x04SPDX\x12\n\n\x02id\x18\x01 \x01(\t\x12\x1c\n\tfull_name\x18\x02 \x01(\tR\tfull_name\x12 \n\x0b\x64\x65tails_url\x18\x04 \x01(\tR\x0b\x64\x65tails_url\x12$\n\rreference_url\x18\x05 \x01(\tR\rreference_url\x12$\n\ris_deprecated\x18\x06 \x01(\x08R\ris_deprecated\x12\"\n\x0cis_fsf_libre\x18\x07 \x01(\x08R\x0cis_fsf_libre\x12(\n\x0fis_osi_approved\x18\x08 \x01(\x08R\x0fis_osi_approved\x12\x1a\n\x08see_also\x18\t \x03(\tR\x08see_also\x12J\n\ncross_refs\x18\n \x03(\x0b\x32*.scanoss.api.licenses.v2.SPDX.SPDXCrossRefR\ncross_refs\x12?\n\nexceptions\x18\x0b \x03(\x0b\x32+.scanoss.api.licenses.v2.SPDX.SPDXException\x1a\xac\x01\n\x0cSPDXCrossRef\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x1a\n\x08is_valid\x18\x02 \x01(\x08R\x08is_valid\x12\x18\n\x07is_live\x18\x03 \x01(\x08R\x07is_live\x12\x11\n\ttimestamp\x18\x04 \x01(\t\x12(\n\x0fis_wayback_link\x18\x05 \x01(\x08R\x0fis_wayback_link\x12\r\n\x05order\x18\x06 \x01(\x05\x12\r\n\x05match\x18\x07 \x01(\t\x1a\x9d\x01\n\rSPDXException\x12\n\n\x02id\x18\x01 \x01(\t\x12\x1c\n\tfull_name\x18\x02 \x01(\tR\tfull_name\x12 \n\x0b\x64\x65tails_url\x18\x03 \x01(\tR\x0b\x64\x65tails_url\x12\x1a\n\x08see_also\x18\x05 \x03(\tR\x08see_also\x12$\n\ris_deprecated\x18\x06 \x01(\x08R\ris_deprecated\"\xfc\x02\n\x05OSADL\x12(\n\x0f\x63opyleft_clause\x18\x01 \x01(\x08R\x0f\x63opyleft_clause\x12\"\n\x0cpatent_hints\x18\x02 \x01(\x08R\x0cpatent_hints\x12\x15\n\rcompatibility\x18\x03 \x03(\t\x12\x38\n\x17\x64\x65pending_compatibility\x18\x04 \x03(\tR\x17\x64\x65pending_compatibility\x12\x17\n\x0fincompatibility\x18\x05 \x03(\t\x12I\n\tuse_cases\x18\x06 \x03(\x0b\x32+.scanoss.api.licenses.v2.OSADL.OSADLUseCaseR\tuse_cases\x1ap\n\x0cOSADLUseCase\x12\x0c\n\x04name\x18\x01 \x01(\t\x12(\n\x0fobligation_text\x18\x02 \x01(\tR\x0fobligation_text\x12(\n\x0fobligation_json\x18\x03 \x01(\tR\x0fobligation_json\"\x86\x01\n\x0bLicenseInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x1c\n\tfull_name\x18\x02 \x01(\tR\tfull_name:M\x92\x41J\nHJF{\"id\": \"GPL-2.0\", \"full_name\": \"GNU General Public License v2.0 only\"}\"\xbe\x01\n\x0eLicenseDetails\x12\x1c\n\tfull_name\x18\x01 \x01(\tR\tfull_name\x12\x32\n\x04type\x18\x02 \x01(\x0e\x32$.scanoss.api.licenses.v2.LicenseType\x12+\n\x04spdx\x18\x03 \x01(\x0b\x32\x1d.scanoss.api.licenses.v2.SPDX\x12-\n\x05osadl\x18\x04 \x01(\x0b\x32\x1e.scanoss.api.licenses.v2.OSADL\"\x1c\n\x0eLicenseRequest\x12\n\n\x02id\x18\x01 \x01(\t\"\x95\x01\n\x14\x43omponentLicenseInfo\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x13\n\x0brequirement\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x11\n\tstatement\x18\x04 \x01(\t\x12\x36\n\x08licenses\x18\x05 \x03(\x0b\x32$.scanoss.api.licenses.v2.LicenseInfo*l\n\x0bLicenseType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0e\n\nPERMISSIVE\x10\x01\x12\x0c\n\x08\x43OPYLEFT\x10\x02\x12\x0e\n\nCOMMERCIAL\x10\x03\x12\x0f\n\x0bPROPRIETARY\x10\x04\x12\x11\n\rPUBLIC_DOMAIN\x10\x05\x32\xbc\x05\n\x07License\x12m\n\x04\x45\x63ho\x12\".scanoss.api.common.v2.EchoRequest\x1a#.scanoss.api.common.v2.EchoResponse\"\x1c\x82\xd3\xe4\x93\x02\x16\"\x11/v2/licenses/echo:\x01*\x12\x92\x01\n\x14GetComponentLicenses\x12\'.scanoss.api.common.v2.ComponentRequest\x1a\x31.scanoss.api.licenses.v2.ComponentLicenseResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/v2/licenses/component\x12\x99\x01\n\x15GetComponentsLicenses\x12(.scanoss.api.common.v2.ComponentsRequest\x1a\x32.scanoss.api.licenses.v2.ComponentsLicenseResponse\"\"\x82\xd3\xe4\x93\x02\x1c\"\x17/v2/licenses/components:\x01*\x12\x84\x01\n\nGetDetails\x12\'.scanoss.api.licenses.v2.LicenseRequest\x1a/.scanoss.api.licenses.v2.LicenseDetailsResponse\"\x1c\x82\xd3\xe4\x93\x02\x16\x12\x14/v2/licenses/details\x12\x89\x01\n\x0eGetObligations\x12\'.scanoss.api.licenses.v2.LicenseRequest\x1a,.scanoss.api.licenses.v2.ObligationsResponse\" \x82\xd3\xe4\x93\x02\x1a\x12\x18/v2/licenses/obligationsB\xa7\x02Z1github.amrom.workers.dev/scanoss/papi/api/licensesv2;licensesv2\x92\x41\xf0\x01\x12\xb4\x01\n\x17SCANOSS License Service\x12\x46License service provides license intelligence for software components.\"L\n\x10scanoss-licenses\x12#https://github.com/scanoss/licenses\x1a\x13support@scanoss.com2\x03\x32.0\x1a\x0f\x61pi.scanoss.com*\x02\x01\x02\x32\x10\x61pplication/json:\x10\x61pplication/jsonb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'scanoss.api.licenses.v2.scanoss_licenses_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z1github.amrom.workers.dev/scanoss/papi/api/licensesv2;licensesv2\222A\360\001\022\264\001\n\027SCANOSS License Service\022FLicense service provides license intelligence for software components.\"L\n\020scanoss-licenses\022#https://github.com/scanoss/licenses\032\023support@scanoss.com2\0032.0\032\017api.scanoss.com*\002\001\0022\020application/json:\020application/json' + _globals['_COMPONENTLICENSERESPONSE']._loaded_options = None + _globals['_COMPONENTLICENSERESPONSE']._serialized_options = b'\222A\243\002\n\240\002J\235\002{\"component\":{\"purl\": \"pkg:github/scanoss/engine@1.0.0\", \"requirement\": \"\", \"version\": \"1.0.0\", \"statement\": \"GPL-2.0\", \"licenses\": [{\"id\": \"GPL-2.0\", \"full_name\": \"GNU General Public License v2.0 only\"}]}, \"status\": {\"status\": \"SUCCESS\", \"message\": \"Licenses Successfully retrieved\"}}' + _globals['_COMPONENTSLICENSERESPONSE']._loaded_options = None + _globals['_COMPONENTSLICENSERESPONSE']._serialized_options = b'\222A\315\003\n\312\003J\307\003{\"components\":[{\"purl\": \"pkg:github/scanoss/engine@1.0.0\", \"requirement\": \"\", \"version\": \"1.0.0\", \"statement\": \"GPL-2.0\", \"licenses\": [{\"id\": \"GPL-2.0\", \"full_name\": \"GNU General Public License v2.0 only\"}]}, {\"purl\": \"pkg:github/scanoss/scanoss.py@v1.30.0\",\"requirement\": \"\",\"version\": \"v1.30.0\",\"statement\": \"MIT\", \"licenses\": [{\"id\": \"MIT\",\"full_name\": \"MIT License\"}]} ], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Licenses Successfully retrieved\"}}' + _globals['_LICENSEINFO']._loaded_options = None + _globals['_LICENSEINFO']._serialized_options = b'\222AJ\nHJF{\"id\": \"GPL-2.0\", \"full_name\": \"GNU General Public License v2.0 only\"}' + _globals['_LICENSE'].methods_by_name['Echo']._loaded_options = None + _globals['_LICENSE'].methods_by_name['Echo']._serialized_options = b'\202\323\344\223\002\026\"\021/v2/licenses/echo:\001*' + _globals['_LICENSE'].methods_by_name['GetComponentLicenses']._loaded_options = None + _globals['_LICENSE'].methods_by_name['GetComponentLicenses']._serialized_options = b'\202\323\344\223\002\030\022\026/v2/licenses/component' + _globals['_LICENSE'].methods_by_name['GetComponentsLicenses']._loaded_options = None + _globals['_LICENSE'].methods_by_name['GetComponentsLicenses']._serialized_options = b'\202\323\344\223\002\034\"\027/v2/licenses/components:\001*' + _globals['_LICENSE'].methods_by_name['GetDetails']._loaded_options = None + _globals['_LICENSE'].methods_by_name['GetDetails']._serialized_options = b'\202\323\344\223\002\026\022\024/v2/licenses/details' + _globals['_LICENSE'].methods_by_name['GetObligations']._loaded_options = None + _globals['_LICENSE'].methods_by_name['GetObligations']._serialized_options = b'\202\323\344\223\002\032\022\030/v2/licenses/obligations' + _globals['_LICENSETYPE']._serialized_start=3175 + _globals['_LICENSETYPE']._serialized_end=3283 + _globals['_COMPONENTLICENSERESPONSE']._serialized_start=198 + _globals['_COMPONENTLICENSERESPONSE']._serialized_end=643 + _globals['_COMPONENTSLICENSERESPONSE']._serialized_start=646 + _globals['_COMPONENTSLICENSERESPONSE']._serialized_end=1263 + _globals['_LICENSEDETAILSRESPONSE']._serialized_start=1266 + _globals['_LICENSEDETAILSRESPONSE']._serialized_end=1403 + _globals['_OBLIGATIONSRESPONSE']._serialized_start=1406 + _globals['_OBLIGATIONSRESPONSE']._serialized_end=1535 + _globals['_SPDX']._serialized_start=1538 + _globals['_SPDX']._serialized_end=2278 + _globals['_SPDX_SPDXCROSSREF']._serialized_start=1946 + _globals['_SPDX_SPDXCROSSREF']._serialized_end=2118 + _globals['_SPDX_SPDXEXCEPTION']._serialized_start=2121 + _globals['_SPDX_SPDXEXCEPTION']._serialized_end=2278 + _globals['_OSADL']._serialized_start=2281 + _globals['_OSADL']._serialized_end=2661 + _globals['_OSADL_OSADLUSECASE']._serialized_start=2549 + _globals['_OSADL_OSADLUSECASE']._serialized_end=2661 + _globals['_LICENSEINFO']._serialized_start=2664 + _globals['_LICENSEINFO']._serialized_end=2798 + _globals['_LICENSEDETAILS']._serialized_start=2801 + _globals['_LICENSEDETAILS']._serialized_end=2991 + _globals['_LICENSEREQUEST']._serialized_start=2993 + _globals['_LICENSEREQUEST']._serialized_end=3021 + _globals['_COMPONENTLICENSEINFO']._serialized_start=3024 + _globals['_COMPONENTLICENSEINFO']._serialized_end=3173 + _globals['_LICENSE']._serialized_start=3286 + _globals['_LICENSE']._serialized_end=3986 +# @@protoc_insertion_point(module_scope) diff --git a/src/scanoss/api/licenses/v2/scanoss_licenses_pb2_grpc.py b/src/scanoss/api/licenses/v2/scanoss_licenses_pb2_grpc.py new file mode 100644 index 00000000..ac0df474 --- /dev/null +++ b/src/scanoss/api/licenses/v2/scanoss_licenses_pb2_grpc.py @@ -0,0 +1,302 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 +from scanoss.api.licenses.v2 import scanoss_licenses_pb2 as scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2 + +GRPC_GENERATED_VERSION = '1.73.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in scanoss/api/licenses/v2/scanoss_licenses_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class LicenseStub(object): + """ + License Service Definition + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Echo = channel.unary_unary( + '/scanoss.api.licenses.v2.License/Echo', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, + _registered_method=True) + self.GetComponentLicenses = channel.unary_unary( + '/scanoss.api.licenses.v2.License/GetComponentLicenses', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.ComponentLicenseResponse.FromString, + _registered_method=True) + self.GetComponentsLicenses = channel.unary_unary( + '/scanoss.api.licenses.v2.License/GetComponentsLicenses', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.ComponentsLicenseResponse.FromString, + _registered_method=True) + self.GetDetails = channel.unary_unary( + '/scanoss.api.licenses.v2.License/GetDetails', + request_serializer=scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.LicenseRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.LicenseDetailsResponse.FromString, + _registered_method=True) + self.GetObligations = channel.unary_unary( + '/scanoss.api.licenses.v2.License/GetObligations', + request_serializer=scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.LicenseRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.ObligationsResponse.FromString, + _registered_method=True) + + +class LicenseServicer(object): + """ + License Service Definition + """ + + def Echo(self, request, context): + """ + Returns the same message that was sent, used for health checks and connectivity testing + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentLicenses(self, request, context): + """ + Get license information for a single software component. + + Examines source code, license files, and package metadata to determine which licenses apply to the component. + Returns license data in both individual SPDX license and SPDX expressions when determinable. + + See: https://github.com/scanoss/papi/blob/main/protobuf/scanoss/api/licenses/v2/README.md?tab=readme-ov-file#getcomponentlicenses + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentsLicenses(self, request, context): + """ + Get license information for multiple software components in a single request. + + Examines source code, license files, and package metadata to determine which licenses apply to each component. + Returns license data in both individual SPDX license and SPDX expressions when determinable. + + See https://github.com/scanoss/papi/blob/main/protobuf/scanoss/api/licenses/v2/README.md?tab=readme-ov-file#getcomponentslicenses + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetDetails(self, request, context): + """ + Get detailed metadata for a specific license by SPDX identifier. + + Provides comprehensive license information including SPDX registry data, + OSADL compliance metadata, license type classification, and official references. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetObligations(self, request, context): + """ + Get compliance obligations and usage requirements for a specific license. + + Returns structured OSADL compliance data including use cases, obligations, + compatibility information, and patent hints for the specified license. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_LicenseServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Echo': grpc.unary_unary_rpc_method_handler( + servicer.Echo, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.FromString, + response_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.SerializeToString, + ), + 'GetComponentLicenses': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentLicenses, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.FromString, + response_serializer=scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.ComponentLicenseResponse.SerializeToString, + ), + 'GetComponentsLicenses': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentsLicenses, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.FromString, + response_serializer=scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.ComponentsLicenseResponse.SerializeToString, + ), + 'GetDetails': grpc.unary_unary_rpc_method_handler( + servicer.GetDetails, + request_deserializer=scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.LicenseRequest.FromString, + response_serializer=scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.LicenseDetailsResponse.SerializeToString, + ), + 'GetObligations': grpc.unary_unary_rpc_method_handler( + servicer.GetObligations, + request_deserializer=scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.LicenseRequest.FromString, + response_serializer=scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.ObligationsResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'scanoss.api.licenses.v2.License', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('scanoss.api.licenses.v2.License', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class License(object): + """ + License Service Definition + """ + + @staticmethod + def Echo(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.licenses.v2.License/Echo', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentLicenses(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.licenses.v2.License/GetComponentLicenses', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.ComponentLicenseResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentsLicenses(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.licenses.v2.License/GetComponentsLicenses', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.ComponentsLicenseResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetDetails(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.licenses.v2.License/GetDetails', + scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.LicenseRequest.SerializeToString, + scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.LicenseDetailsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetObligations(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.licenses.v2.License/GetObligations', + scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.LicenseRequest.SerializeToString, + scanoss_dot_api_dot_licenses_dot_v2_dot_scanoss__licenses__pb2.ObligationsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/src/scanoss/api/scanning/__init__.py b/src/scanoss/api/scanning/__init__.py index d2ed05b0..c67e64da 100644 --- a/src/scanoss/api/scanning/__init__.py +++ b/src/scanoss/api/scanning/__init__.py @@ -1,23 +1,23 @@ """ - SPDX-License-Identifier: MIT +SPDX-License-Identifier: MIT - Copyright (c) 2021, SCANOSS + Copyright (c) 2021, SCANOSS - 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: + 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 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. + 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/src/scanoss/api/scanning/v2/__init__.py b/src/scanoss/api/scanning/v2/__init__.py index d2ed05b0..c67e64da 100644 --- a/src/scanoss/api/scanning/v2/__init__.py +++ b/src/scanoss/api/scanning/v2/__init__.py @@ -1,23 +1,23 @@ """ - SPDX-License-Identifier: MIT +SPDX-License-Identifier: MIT - Copyright (c) 2021, SCANOSS + Copyright (c) 2021, SCANOSS - 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: + 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 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. + 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/src/scanoss/api/scanning/v2/scanoss_scanning_pb2.py b/src/scanoss/api/scanning/v2/scanoss_scanning_pb2.py index a70366fd..6c61c039 100644 --- a/src/scanoss/api/scanning/v2/scanoss_scanning_pb2.py +++ b/src/scanoss/api/scanning/v2/scanoss_scanning_pb2.py @@ -1,59 +1,60 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: scanoss/api/scanning/v2/scanoss-scanning.proto +# Protobuf Python Version: 6.31.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 0, + '', + 'scanoss/api/scanning/v2/scanoss-scanning.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='scanoss/api/scanning/v2/scanoss-scanning.proto', - package='scanoss.api.scanning.v2', - syntax='proto3', - serialized_options=b'Z1github.amrom.workers.dev/scanoss/papi/api/scanningv2;scanningv2', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n.scanoss/api/scanning/v2/scanoss-scanning.proto\x12\x17scanoss.api.scanning.v2\x1a*scanoss/api/common/v2/scanoss-common.proto2[\n\x08Scanning\x12O\n\x04\x45\x63ho\x12\".scanoss.api.common.v2.EchoRequest\x1a#.scanoss.api.common.v2.EchoResponseB3Z1github.amrom.workers.dev/scanoss/papi/api/scanningv2;scanningv2b\x06proto3' - , - dependencies=[scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.DESCRIPTOR,]) - - - -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - - -DESCRIPTOR._options = None - -_SCANNING = _descriptor.ServiceDescriptor( - name='Scanning', - full_name='scanoss.api.scanning.v2.Scanning', - file=DESCRIPTOR, - index=0, - serialized_options=None, - create_key=_descriptor._internal_create_key, - serialized_start=119, - serialized_end=210, - methods=[ - _descriptor.MethodDescriptor( - name='Echo', - full_name='scanoss.api.scanning.v2.Scanning.Echo', - index=0, - containing_service=None, - input_type=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2._ECHOREQUEST, - output_type=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2._ECHORESPONSE, - serialized_options=None, - create_key=_descriptor._internal_create_key, - ), -]) -_sym_db.RegisterServiceDescriptor(_SCANNING) - -DESCRIPTOR.services_by_name['Scanning'] = _SCANNING - +from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 +from protoc_gen_openapiv2.options import annotations_pb2 as protoc__gen__openapiv2_dot_options_dot_annotations__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.scanoss/api/scanning/v2/scanoss-scanning.proto\x12\x17scanoss.api.scanning.v2\x1a*scanoss/api/common/v2/scanoss-common.proto\x1a\x1cgoogle/api/annotations.proto\x1a.protoc-gen-openapiv2/options/annotations.proto\"\xde\x04\n\nHFHRequest\x12:\n\x04root\x18\x01 \x01(\x0b\x32,.scanoss.api.scanning.v2.HFHRequest.Children\x12&\n\x0erank_threshold\x18\x02 \x01(\x05R\x0erank_threshold\x12\x10\n\x08\x63\x61tegory\x18\x03 \x01(\t\x12\x13\n\x0bquery_limit\x18\x04 \x01(\x05\x12\x1b\n\x13recursive_threshold\x18\x05 \x01(\x02\x12\x1a\n\x12min_accepted_score\x18\x06 \x01(\x02\x1a\x8b\x03\n\x08\x43hildren\x12\x18\n\x07path_id\x18\x01 \x01(\tR\x07path_id\x12&\n\x0esim_hash_names\x18\x02 \x01(\tR\x0esim_hash_names\x12*\n\x10sim_hash_content\x18\x03 \x01(\tR\x10sim_hash_content\x12>\n\x08\x63hildren\x18\x04 \x03(\x0b\x32,.scanoss.api.scanning.v2.HFHRequest.Children\x12.\n\x12sim_hash_dir_names\x18\x05 \x01(\tR\x12sim_hash_dir_names\x12j\n\x0flang_extensions\x18\x06 \x03(\x0b\x32@.scanoss.api.scanning.v2.HFHRequest.Children.LangExtensionsEntryR\x0flang_extensions\x1a\x35\n\x13LangExtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"\xac\x03\n\x0bHFHResponse\x12<\n\x07results\x18\x01 \x03(\x0b\x32+.scanoss.api.scanning.v2.HFHResponse.Result\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1a)\n\x07Version\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\r\n\x05score\x18\x02 \x01(\x02\x1a\x94\x01\n\tComponent\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06vendor\x18\x03 \x01(\t\x12>\n\x08versions\x18\x04 \x03(\x0b\x32,.scanoss.api.scanning.v2.HFHResponse.Version\x12\x0c\n\x04rank\x18\x05 \x01(\x05\x12\r\n\x05order\x18\x06 \x01(\x05\x1a\x66\n\x06Result\x12\x18\n\x07path_id\x18\x01 \x01(\tR\x07path_id\x12\x42\n\ncomponents\x18\x02 \x03(\x0b\x32..scanoss.api.scanning.v2.HFHResponse.Component2\xf8\x01\n\x08Scanning\x12m\n\x04\x45\x63ho\x12\".scanoss.api.common.v2.EchoRequest\x1a#.scanoss.api.common.v2.EchoResponse\"\x1c\x82\xd3\xe4\x93\x02\x16\"\x11/v2/scanning/echo:\x01*\x12}\n\x0e\x46olderHashScan\x12#.scanoss.api.scanning.v2.HFHRequest\x1a$.scanoss.api.scanning.v2.HFHResponse\" \x82\xd3\xe4\x93\x02\x1a\"\x15/v2/scanning/hfh/scan:\x01*B\x8a\x02Z1github.amrom.workers.dev/scanoss/papi/api/scanningv2;scanningv2\x92\x41\xd3\x01\x12m\n\x18SCANOSS Scanning Service\"L\n\x10scanoss-scanning\x12#https://github.com/scanoss/scanning\x1a\x13support@scanoss.com2\x03\x32.0*\x01\x01\x32\x10\x61pplication/json:\x10\x61pplication/jsonR;\n\x03\x34\x30\x34\x12\x34\n*Returned when the resource does not exist.\x12\x06\n\x04\x9a\x02\x01\x07\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'scanoss.api.scanning.v2.scanoss_scanning_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z1github.amrom.workers.dev/scanoss/papi/api/scanningv2;scanningv2\222A\323\001\022m\n\030SCANOSS Scanning Service\"L\n\020scanoss-scanning\022#https://github.com/scanoss/scanning\032\023support@scanoss.com2\0032.0*\001\0012\020application/json:\020application/jsonR;\n\003404\0224\n*Returned when the resource does not exist.\022\006\n\004\232\002\001\007' + _globals['_HFHREQUEST_CHILDREN_LANGEXTENSIONSENTRY']._loaded_options = None + _globals['_HFHREQUEST_CHILDREN_LANGEXTENSIONSENTRY']._serialized_options = b'8\001' + _globals['_SCANNING'].methods_by_name['Echo']._loaded_options = None + _globals['_SCANNING'].methods_by_name['Echo']._serialized_options = b'\202\323\344\223\002\026\"\021/v2/scanning/echo:\001*' + _globals['_SCANNING'].methods_by_name['FolderHashScan']._loaded_options = None + _globals['_SCANNING'].methods_by_name['FolderHashScan']._serialized_options = b'\202\323\344\223\002\032\"\025/v2/scanning/hfh/scan:\001*' + _globals['_HFHREQUEST']._serialized_start=198 + _globals['_HFHREQUEST']._serialized_end=804 + _globals['_HFHREQUEST_CHILDREN']._serialized_start=409 + _globals['_HFHREQUEST_CHILDREN']._serialized_end=804 + _globals['_HFHREQUEST_CHILDREN_LANGEXTENSIONSENTRY']._serialized_start=751 + _globals['_HFHREQUEST_CHILDREN_LANGEXTENSIONSENTRY']._serialized_end=804 + _globals['_HFHRESPONSE']._serialized_start=807 + _globals['_HFHRESPONSE']._serialized_end=1235 + _globals['_HFHRESPONSE_VERSION']._serialized_start=939 + _globals['_HFHRESPONSE_VERSION']._serialized_end=980 + _globals['_HFHRESPONSE_COMPONENT']._serialized_start=983 + _globals['_HFHRESPONSE_COMPONENT']._serialized_end=1131 + _globals['_HFHRESPONSE_RESULT']._serialized_start=1133 + _globals['_HFHRESPONSE_RESULT']._serialized_end=1235 + _globals['_SCANNING']._serialized_start=1238 + _globals['_SCANNING']._serialized_end=1486 # @@protoc_insertion_point(module_scope) diff --git a/src/scanoss/api/scanning/v2/scanoss_scanning_pb2_grpc.py b/src/scanoss/api/scanning/v2/scanoss_scanning_pb2_grpc.py index f6530e94..6928eed7 100644 --- a/src/scanoss/api/scanning/v2/scanoss_scanning_pb2_grpc.py +++ b/src/scanoss/api/scanning/v2/scanoss_scanning_pb2_grpc.py @@ -1,12 +1,34 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc +import warnings from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 +from scanoss.api.scanning.v2 import scanoss_scanning_pb2 as scanoss_dot_api_dot_scanning_dot_v2_dot_scanoss__scanning__pb2 + +GRPC_GENERATED_VERSION = '1.73.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in scanoss/api/scanning/v2/scanoss_scanning_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) class ScanningStub(object): - """Expose all of the SCANOSS Scanning RPCs here + """* + Expose all of the SCANOSS Scanning RPCs here """ def __init__(self, channel): @@ -19,11 +41,17 @@ def __init__(self, channel): '/scanoss.api.scanning.v2.Scanning/Echo', request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, response_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, - ) + _registered_method=True) + self.FolderHashScan = channel.unary_unary( + '/scanoss.api.scanning.v2.Scanning/FolderHashScan', + request_serializer=scanoss_dot_api_dot_scanning_dot_v2_dot_scanoss__scanning__pb2.HFHRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_scanning_dot_v2_dot_scanoss__scanning__pb2.HFHResponse.FromString, + _registered_method=True) class ScanningServicer(object): - """Expose all of the SCANOSS Scanning RPCs here + """* + Expose all of the SCANOSS Scanning RPCs here """ def Echo(self, request, context): @@ -33,6 +61,13 @@ def Echo(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def FolderHashScan(self, request, context): + """Scan the given folder request looking for matches + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_ScanningServicer_to_server(servicer, server): rpc_method_handlers = { @@ -41,15 +76,22 @@ def add_ScanningServicer_to_server(servicer, server): request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.FromString, response_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.SerializeToString, ), + 'FolderHashScan': grpc.unary_unary_rpc_method_handler( + servicer.FolderHashScan, + request_deserializer=scanoss_dot_api_dot_scanning_dot_v2_dot_scanoss__scanning__pb2.HFHRequest.FromString, + response_serializer=scanoss_dot_api_dot_scanning_dot_v2_dot_scanoss__scanning__pb2.HFHResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'scanoss.api.scanning.v2.Scanning', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('scanoss.api.scanning.v2.Scanning', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. class Scanning(object): - """Expose all of the SCANOSS Scanning RPCs here + """* + Expose all of the SCANOSS Scanning RPCs here """ @staticmethod @@ -63,8 +105,45 @@ def Echo(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/scanoss.api.scanning.v2.Scanning/Echo', + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.scanning.v2.Scanning/Echo', scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def FolderHashScan(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.scanning.v2.Scanning/FolderHashScan', + scanoss_dot_api_dot_scanning_dot_v2_dot_scanoss__scanning__pb2.HFHRequest.SerializeToString, + scanoss_dot_api_dot_scanning_dot_v2_dot_scanoss__scanning__pb2.HFHResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/src/scanoss/api/semgrep/__init__.py b/src/scanoss/api/semgrep/__init__.py new file mode 100644 index 00000000..da9b8e79 --- /dev/null +++ b/src/scanoss/api/semgrep/__init__.py @@ -0,0 +1,23 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2023, SCANOSS + + 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/src/scanoss/api/semgrep/v2/__init__.py b/src/scanoss/api/semgrep/v2/__init__.py new file mode 100644 index 00000000..da9b8e79 --- /dev/null +++ b/src/scanoss/api/semgrep/v2/__init__.py @@ -0,0 +1,23 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2023, SCANOSS + + 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/src/scanoss/api/semgrep/v2/scanoss_semgrep_pb2.py b/src/scanoss/api/semgrep/v2/scanoss_semgrep_pb2.py new file mode 100644 index 00000000..af4a519d --- /dev/null +++ b/src/scanoss/api/semgrep/v2/scanoss_semgrep_pb2.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: scanoss/api/semgrep/v2/scanoss-semgrep.proto +# Protobuf Python Version: 6.31.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 0, + '', + 'scanoss/api/semgrep/v2/scanoss-semgrep.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 +from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 +from protoc_gen_openapiv2.options import annotations_pb2 as protoc__gen__openapiv2_dot_options_dot_annotations__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,scanoss/api/semgrep/v2/scanoss-semgrep.proto\x12\x16scanoss.api.semgrep.v2\x1a*scanoss/api/common/v2/scanoss-common.proto\x1a\x1cgoogle/api/annotations.proto\x1a.protoc-gen-openapiv2/options/annotations.proto\"C\n\x05Issue\x12\x0e\n\x06ruleID\x18\x01 \x01(\t\x12\x0c\n\x04\x66rom\x18\x02 \x01(\t\x12\n\n\x02to\x18\x03 \x01(\t\x12\x10\n\x08severity\x18\x04 \x01(\t\"T\n\x04\x46ile\x12\x0f\n\x07\x66ileMD5\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\x12-\n\x06issues\x18\x03 \x03(\x0b\x32\x1d.scanoss.api.semgrep.v2.Issue\"\xdf\x01\n\x0fSemgrepResponse\x12<\n\x05purls\x18\x01 \x03(\x0b\x32-.scanoss.api.semgrep.v2.SemgrepResponse.Purls\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse\x1aS\n\x05Purls\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12+\n\x05\x66iles\x18\x03 \x03(\x0b\x32\x1c.scanoss.api.semgrep.v2.File:\x02\x18\x01\"u\n\x12\x43omponentIssueInfo\x12\x0c\n\x04purl\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x13\n\x0brequirement\x18\x03 \x01(\t\x12+\n\x05\x66iles\x18\x04 \x03(\x0b\x32\x1c.scanoss.api.semgrep.v2.File\"\xe6\x06\n\x17\x43omponentsIssueResponse\x12>\n\ncomponents\x18\x01 \x03(\x0b\x32*.scanoss.api.semgrep.v2.ComponentIssueInfo\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse:\xd3\x05\x92\x41\xcf\x05\n\xcc\x05J\xc9\x05{\"components\":[{\"purl\":\"pkg:maven/org.apache.commons/commons-lang3\",\"version\":\"3.12.0\",\"requirement\":\"3.12.0\",\"files\":[{\"fileMD5\":\"a1b2c3d4e5f6\",\"path\":\"src/main/java/org/apache/commons/lang3/StringUtils.java\",\"issues\":[{\"ruleID\":\"java.lang.security.audit.crypto.weak-hash\",\"from\":\"156\",\"to\":\"159\",\"severity\":\"WARNING\"},{\"ruleID\":\"java.lang.security.audit.sql-injection.sql-injection\",\"from\":\"284\",\"to\":\"286\",\"severity\":\"ERROR\"}]},{\"fileMD5\":\"b2c3d4e5f6a1\",\"path\":\"src/main/java/org/apache/commons/lang3/Validate.java\",\"issues\":[{\"ruleID\":\"java.lang.security.audit.hardcoded-secret\",\"from\":\"95\",\"to\":\"95\",\"severity\":\"ERROR\"}]}]}],\"status\":{\"status\":\"SUCCESS\",\"message\":\"Security analysis completed successfully\"}}\"\xb9\x04\n\x16\x43omponentIssueResponse\x12=\n\tcomponent\x18\x01 \x01(\x0b\x32*.scanoss.api.semgrep.v2.ComponentIssueInfo\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse:\xa8\x03\x92\x41\xa4\x03\n\xa1\x03J\x9e\x03{\"component\":{\"purl\":\"pkg:maven/org.apache.commons/commons-lang3\",\"version\":\"3.12.0\",\"requirement\":\"3.12.0\",\"files\":[{\"fileMD5\":\"a1b2c3d4e5f6\",\"path\":\"src/main/java/org/apache/commons/lang3/StringUtils.java\",\"issues\":[{\"ruleID\":\"java.lang.security.audit.sql-injection.sql-injection\",\"from\":\"284\",\"to\":\"286\",\"severity\":\"ERROR\"}]}]},\"status\":{\"status\":\"SUCCESS\",\"message\":\"Security analysis completed successfully\"}}2\x89\x04\n\x07Semgrep\x12l\n\x04\x45\x63ho\x12\".scanoss.api.common.v2.EchoRequest\x1a#.scanoss.api.common.v2.EchoResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\"\x10/v2/semgrep/echo:\x01*\x12]\n\tGetIssues\x12\".scanoss.api.common.v2.PurlRequest\x1a\'.scanoss.api.semgrep.v2.SemgrepResponse\"\x03\x88\x02\x01\x12\x9a\x01\n\x13GetComponentsIssues\x12(.scanoss.api.common.v2.ComponentsRequest\x1a/.scanoss.api.semgrep.v2.ComponentsIssueResponse\"(\x82\xd3\xe4\x93\x02\"\"\x1d/v2/semgrep/issues/components:\x01*\x12\x93\x01\n\x12GetComponentIssues\x12\'.scanoss.api.common.v2.ComponentRequest\x1a..scanoss.api.semgrep.v2.ComponentIssueResponse\"$\x82\xd3\xe4\x93\x02\x1e\x12\x1c/v2/semgrep/issues/componentB\x85\x02Z/github.com/scanoss/papi/api/semgrepv2;semgrepv2\x92\x41\xd0\x01\x12j\n\x17SCANOSS Semgrep Service\"J\n\x0fscanoss-semgrep\x12\"https://github.com/scanoss/semgrep\x1a\x13support@scanoss.com2\x03\x32.0*\x01\x01\x32\x10\x61pplication/json:\x10\x61pplication/jsonR;\n\x03\x34\x30\x34\x12\x34\n*Returned when the resource does not exist.\x12\x06\n\x04\x9a\x02\x01\x07\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'scanoss.api.semgrep.v2.scanoss_semgrep_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z/github.com/scanoss/papi/api/semgrepv2;semgrepv2\222A\320\001\022j\n\027SCANOSS Semgrep Service\"J\n\017scanoss-semgrep\022\"https://github.com/scanoss/semgrep\032\023support@scanoss.com2\0032.0*\001\0012\020application/json:\020application/jsonR;\n\003404\0224\n*Returned when the resource does not exist.\022\006\n\004\232\002\001\007' + _globals['_SEMGREPRESPONSE']._loaded_options = None + _globals['_SEMGREPRESPONSE']._serialized_options = b'\030\001' + _globals['_COMPONENTSISSUERESPONSE']._loaded_options = None + _globals['_COMPONENTSISSUERESPONSE']._serialized_options = b'\222A\317\005\n\314\005J\311\005{\"components\":[{\"purl\":\"pkg:maven/org.apache.commons/commons-lang3\",\"version\":\"3.12.0\",\"requirement\":\"3.12.0\",\"files\":[{\"fileMD5\":\"a1b2c3d4e5f6\",\"path\":\"src/main/java/org/apache/commons/lang3/StringUtils.java\",\"issues\":[{\"ruleID\":\"java.lang.security.audit.crypto.weak-hash\",\"from\":\"156\",\"to\":\"159\",\"severity\":\"WARNING\"},{\"ruleID\":\"java.lang.security.audit.sql-injection.sql-injection\",\"from\":\"284\",\"to\":\"286\",\"severity\":\"ERROR\"}]},{\"fileMD5\":\"b2c3d4e5f6a1\",\"path\":\"src/main/java/org/apache/commons/lang3/Validate.java\",\"issues\":[{\"ruleID\":\"java.lang.security.audit.hardcoded-secret\",\"from\":\"95\",\"to\":\"95\",\"severity\":\"ERROR\"}]}]}],\"status\":{\"status\":\"SUCCESS\",\"message\":\"Security analysis completed successfully\"}}' + _globals['_COMPONENTISSUERESPONSE']._loaded_options = None + _globals['_COMPONENTISSUERESPONSE']._serialized_options = b'\222A\244\003\n\241\003J\236\003{\"component\":{\"purl\":\"pkg:maven/org.apache.commons/commons-lang3\",\"version\":\"3.12.0\",\"requirement\":\"3.12.0\",\"files\":[{\"fileMD5\":\"a1b2c3d4e5f6\",\"path\":\"src/main/java/org/apache/commons/lang3/StringUtils.java\",\"issues\":[{\"ruleID\":\"java.lang.security.audit.sql-injection.sql-injection\",\"from\":\"284\",\"to\":\"286\",\"severity\":\"ERROR\"}]}]},\"status\":{\"status\":\"SUCCESS\",\"message\":\"Security analysis completed successfully\"}}' + _globals['_SEMGREP'].methods_by_name['Echo']._loaded_options = None + _globals['_SEMGREP'].methods_by_name['Echo']._serialized_options = b'\202\323\344\223\002\025\"\020/v2/semgrep/echo:\001*' + _globals['_SEMGREP'].methods_by_name['GetIssues']._loaded_options = None + _globals['_SEMGREP'].methods_by_name['GetIssues']._serialized_options = b'\210\002\001' + _globals['_SEMGREP'].methods_by_name['GetComponentsIssues']._loaded_options = None + _globals['_SEMGREP'].methods_by_name['GetComponentsIssues']._serialized_options = b'\202\323\344\223\002\"\"\035/v2/semgrep/issues/components:\001*' + _globals['_SEMGREP'].methods_by_name['GetComponentIssues']._loaded_options = None + _globals['_SEMGREP'].methods_by_name['GetComponentIssues']._serialized_options = b'\202\323\344\223\002\036\022\034/v2/semgrep/issues/component' + _globals['_ISSUE']._serialized_start=194 + _globals['_ISSUE']._serialized_end=261 + _globals['_FILE']._serialized_start=263 + _globals['_FILE']._serialized_end=347 + _globals['_SEMGREPRESPONSE']._serialized_start=350 + _globals['_SEMGREPRESPONSE']._serialized_end=573 + _globals['_SEMGREPRESPONSE_PURLS']._serialized_start=486 + _globals['_SEMGREPRESPONSE_PURLS']._serialized_end=569 + _globals['_COMPONENTISSUEINFO']._serialized_start=575 + _globals['_COMPONENTISSUEINFO']._serialized_end=692 + _globals['_COMPONENTSISSUERESPONSE']._serialized_start=695 + _globals['_COMPONENTSISSUERESPONSE']._serialized_end=1565 + _globals['_COMPONENTISSUERESPONSE']._serialized_start=1568 + _globals['_COMPONENTISSUERESPONSE']._serialized_end=2137 + _globals['_SEMGREP']._serialized_start=2140 + _globals['_SEMGREP']._serialized_end=2661 +# @@protoc_insertion_point(module_scope) diff --git a/src/scanoss/api/semgrep/v2/scanoss_semgrep_pb2_grpc.py b/src/scanoss/api/semgrep/v2/scanoss_semgrep_pb2_grpc.py new file mode 100644 index 00000000..2b7e6c10 --- /dev/null +++ b/src/scanoss/api/semgrep/v2/scanoss_semgrep_pb2_grpc.py @@ -0,0 +1,243 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 +from scanoss.api.semgrep.v2 import scanoss_semgrep_pb2 as scanoss_dot_api_dot_semgrep_dot_v2_dot_scanoss__semgrep__pb2 + +GRPC_GENERATED_VERSION = '1.73.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in scanoss/api/semgrep/v2/scanoss_semgrep_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class SemgrepStub(object): + """* + Expose all of the SCANOSS Semgrep Security Analysis RPCs here + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Echo = channel.unary_unary( + '/scanoss.api.semgrep.v2.Semgrep/Echo', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, + _registered_method=True) + self.GetIssues = channel.unary_unary( + '/scanoss.api.semgrep.v2.Semgrep/GetIssues', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_semgrep_dot_v2_dot_scanoss__semgrep__pb2.SemgrepResponse.FromString, + _registered_method=True) + self.GetComponentsIssues = channel.unary_unary( + '/scanoss.api.semgrep.v2.Semgrep/GetComponentsIssues', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_semgrep_dot_v2_dot_scanoss__semgrep__pb2.ComponentsIssueResponse.FromString, + _registered_method=True) + self.GetComponentIssues = channel.unary_unary( + '/scanoss.api.semgrep.v2.Semgrep/GetComponentIssues', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_semgrep_dot_v2_dot_scanoss__semgrep__pb2.ComponentIssueResponse.FromString, + _registered_method=True) + + +class SemgrepServicer(object): + """* + Expose all of the SCANOSS Semgrep Security Analysis RPCs here + """ + + def Echo(self, request, context): + """Standard health check endpoint to verify service availability and connectivity + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetIssues(self, request, context): + """[DEPRECATED] Get potential security issues associated with a list of PURLs + This method accepts PURL-based requests and is deprecated in favor of GetComponentsIssues + which accepts ComponentsRequest for better component identification + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentsIssues(self, request, context): + """Get potential security issues associated with multiple components + This is the current method that accepts ComponentsRequest for enhanced component identification + Replaces the deprecated GetIssues method + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentIssues(self, request, context): + """Get potential security issues associated with a single component + This is the current method that accepts ComponentRequest for enhanced component identification + Replaces the deprecated GetIssues method for single component queries + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_SemgrepServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Echo': grpc.unary_unary_rpc_method_handler( + servicer.Echo, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.FromString, + response_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.SerializeToString, + ), + 'GetIssues': grpc.unary_unary_rpc_method_handler( + servicer.GetIssues, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.FromString, + response_serializer=scanoss_dot_api_dot_semgrep_dot_v2_dot_scanoss__semgrep__pb2.SemgrepResponse.SerializeToString, + ), + 'GetComponentsIssues': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentsIssues, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.FromString, + response_serializer=scanoss_dot_api_dot_semgrep_dot_v2_dot_scanoss__semgrep__pb2.ComponentsIssueResponse.SerializeToString, + ), + 'GetComponentIssues': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentIssues, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.FromString, + response_serializer=scanoss_dot_api_dot_semgrep_dot_v2_dot_scanoss__semgrep__pb2.ComponentIssueResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'scanoss.api.semgrep.v2.Semgrep', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('scanoss.api.semgrep.v2.Semgrep', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class Semgrep(object): + """* + Expose all of the SCANOSS Semgrep Security Analysis RPCs here + """ + + @staticmethod + def Echo(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.semgrep.v2.Semgrep/Echo', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetIssues(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.semgrep.v2.Semgrep/GetIssues', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.PurlRequest.SerializeToString, + scanoss_dot_api_dot_semgrep_dot_v2_dot_scanoss__semgrep__pb2.SemgrepResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentsIssues(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.semgrep.v2.Semgrep/GetComponentsIssues', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + scanoss_dot_api_dot_semgrep_dot_v2_dot_scanoss__semgrep__pb2.ComponentsIssueResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentIssues(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.semgrep.v2.Semgrep/GetComponentIssues', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + scanoss_dot_api_dot_semgrep_dot_v2_dot_scanoss__semgrep__pb2.ComponentIssueResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/src/scanoss/api/vulnerabilities/__init__.py b/src/scanoss/api/vulnerabilities/__init__.py new file mode 100644 index 00000000..8c08839c --- /dev/null +++ b/src/scanoss/api/vulnerabilities/__init__.py @@ -0,0 +1,23 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2022, SCANOSS + + 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/src/scanoss/api/vulnerabilities/v2/__init__.py b/src/scanoss/api/vulnerabilities/v2/__init__.py new file mode 100644 index 00000000..8c08839c --- /dev/null +++ b/src/scanoss/api/vulnerabilities/v2/__init__.py @@ -0,0 +1,23 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2022, SCANOSS + + 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/src/scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py b/src/scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py new file mode 100644 index 00000000..4a1fc7dc --- /dev/null +++ b/src/scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: scanoss/api/vulnerabilities/v2/scanoss-vulnerabilities.proto +# Protobuf Python Version: 6.31.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 0, + '', + 'scanoss/api/vulnerabilities/v2/scanoss-vulnerabilities.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 +from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 +from protoc_gen_openapiv2.options import annotations_pb2 as protoc__gen__openapiv2_dot_options_dot_annotations__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n1.0.0\", \"version\": \"1.0.0\", \"vulnerabilities\": [{\"id\": \"DLA-2640-1\", \"cve\": \"DLA-2640-1\", \"url\": \"https://osv.dev/vulnerability/DLA-2640-1\", \"summary\": \"gst-plugins-good1.0 - security update\", \"severity\": \"Critical\", \"published\": \"2021-04-26\", \"modified\": \"2025-05-26\", \"source\": \"OSV\", \"cvss\": [{\"cvss\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\", \"cvss_score\": 9.8, \"cvss_severity\": \"Critical\"}]}]}, \"status\": {\"status\": \"SUCCESS\", \"message\": \"Vulnerabilities Successfully retrieved\"}}\"\xbd\t\n\x1f\x43omponentsVulnerabilityResponse\x12N\n\ncomponents\x18\x01 \x03(\x0b\x32:.scanoss.api.vulnerabilities.v2.ComponentVulnerabilityInfo\x12\x35\n\x06status\x18\x02 \x01(\x0b\x32%.scanoss.api.common.v2.StatusResponse:\x92\x08\x92\x41\x8e\x08\n\x8b\x08J\x88\x08{\"components\":[{\"purl\": \"pkg:github/scanoss/engine\", \"requirement\": \"1.0.0\", \"version\": \"1.0.0\", \"vulnerabilities\": [{\"id\": \"DLA-2640-1\", \"cve\": \"DLA-2640-1\", \"url\": \"https://osv.dev/vulnerability/DLA-2640-1\", \"summary\": \"gst-plugins-good1.0 - security update\", \"severity\": \"Critical\", \"published\": \"2021-04-26\", \"modified\": \"2025-05-26\", \"source\": \"OSV\", \"cvss\": [{\"cvss\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\", \"cvss_score\": 9.8, \"cvss_severity\": \"Critical\"}]}]}, {\"purl\": \"pkg:github/scanoss/scanoss.py\",\"requirement\": \"v1.30.0\",\"version\": \"v1.30.0\", \"vulnerabilities\": [{\"id\": \"CVE-2024-54321\", \"cve\": \"CVE-2024-54321\", \"url\": \"https://nvd.nist.gov/vuln/detail/CVE-2024-54321\", \"summary\": \"Denial of service vulnerability\", \"severity\": \"Medium\", \"published\": \"2024-01-15\", \"modified\": \"2024-02-01\", \"source\": \"NDV\", \"cvss\": [{\"cvss\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L\", \"cvss_score\": 4.3, \"cvss_severity\": \"Medium\"}]}]}], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Vulnerabilities Successfully retrieved\"}}2\xb3\x08\n\x0fVulnerabilities\x12t\n\x04\x45\x63ho\x12\".scanoss.api.common.v2.EchoRequest\x1a#.scanoss.api.common.v2.EchoResponse\"#\x82\xd3\xe4\x93\x02\x1d\"\x18/v2/vulnerabilities/echo:\x01*\x12q\n\x07GetCpes\x12\x34.scanoss.api.vulnerabilities.v2.VulnerabilityRequest\x1a+.scanoss.api.vulnerabilities.v2.CpeResponse\"\x03\x88\x02\x01\x12\x9e\x01\n\x10GetComponentCpes\x12\'.scanoss.api.common.v2.ComponentRequest\x1a\x35.scanoss.api.vulnerabilities.v2.ComponentCpesResponse\"*\x82\xd3\xe4\x93\x02$\x12\"/v2/vulnerabilities/cpes/component\x12\xa5\x01\n\x11GetComponentsCpes\x12(.scanoss.api.common.v2.ComponentsRequest\x1a\x36.scanoss.api.vulnerabilities.v2.ComponentsCpesResponse\".\x82\xd3\xe4\x93\x02(\"#/v2/vulnerabilities/cpes/components:\x01*\x12\x86\x01\n\x12GetVulnerabilities\x12\x34.scanoss.api.vulnerabilities.v2.VulnerabilityRequest\x1a\x35.scanoss.api.vulnerabilities.v2.VulnerabilityResponse\"\x03\x88\x02\x01\x12\xad\x01\n\x1bGetComponentVulnerabilities\x12\'.scanoss.api.common.v2.ComponentRequest\x1a>.scanoss.api.vulnerabilities.v2.ComponentVulnerabilityResponse\"%\x82\xd3\xe4\x93\x02\x1f\x12\x1d/v2/vulnerabilities/component\x12\xb4\x01\n\x1cGetComponentsVulnerabilities\x12(.scanoss.api.common.v2.ComponentsRequest\x1a?.scanoss.api.vulnerabilities.v2.ComponentsVulnerabilityResponse\")\x82\xd3\xe4\x93\x02#\"\x1e/v2/vulnerabilities/components:\x01*B\x92\x03Z?github.com/scanoss/papi/api/vulnerabilitiesv2;vulnerabilitiesv2\x92\x41\xcd\x02\x12\xd4\x01\n\x1dSCANOSS Vulnerability Service\x12RVulnerability service provides vulnerability intelligence for software components.\"Z\n\x17scanoss-vulnerabilities\x12*https://github.com/scanoss/vulnerabilities\x1a\x13support@scanoss.com2\x03\x32.0\x1a\x0f\x61pi.scanoss.com*\x02\x01\x02\x32\x10\x61pplication/json:\x10\x61pplication/jsonR;\n\x03\x34\x30\x34\x12\x34\n*Returned when the resource does not exist.\x12\x06\n\x04\x9a\x02\x01\x07\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'scanoss.api.vulnerabilities.v2.scanoss_vulnerabilities_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z?github.com/scanoss/papi/api/vulnerabilitiesv2;vulnerabilitiesv2\222A\315\002\022\324\001\n\035SCANOSS Vulnerability Service\022RVulnerability service provides vulnerability intelligence for software components.\"Z\n\027scanoss-vulnerabilities\022*https://github.com/scanoss/vulnerabilities\032\023support@scanoss.com2\0032.0\032\017api.scanoss.com*\002\001\0022\020application/json:\020application/jsonR;\n\003404\0224\n*Returned when the resource does not exist.\022\006\n\004\232\002\001\007' + _globals['_VULNERABILITYREQUEST']._loaded_options = None + _globals['_VULNERABILITYREQUEST']._serialized_options = b'\030\001' + _globals['_CPERESPONSE']._loaded_options = None + _globals['_CPERESPONSE']._serialized_options = b'\030\001' + _globals['_COMPONENTCPESRESPONSE']._loaded_options = None + _globals['_COMPONENTCPESRESPONSE']._serialized_options = b'\222A\360\001\n\355\001J\352\001{\"component\":{\"purl\": \"pkg:github/scanoss/engine@1.0.0\", \"requirement\": \"1.0.0\", \"version\": \"1.0.0\", \"cpes\": [\"cpe:2.3:a:scanoss:engine:1.0.0:*:*:*:*:*:*:*\"]}, \"status\": {\"status\": \"SUCCESS\", \"message\": \"CPEs Successfully retrieved\"}}' + _globals['_COMPONENTSCPESRESPONSE']._loaded_options = None + _globals['_COMPONENTSCPESRESPONSE']._serialized_options = b'\222A\212\003\n\207\003J\204\003{\"components\":[{\"purl\": \"pkg:github/scanoss/engine\", \"requirement=\": \"1.0.0\", \"version=\": \"1.0.0\", \"cpes\": [\"cpe:2.3:a:scanoss:engine:1.0.0:*:*:*:*:*:*:*\"]}, {\"purl\": \"pkg:github/scanoss/scanoss.py@v1.30.0\",\"requirement\": \"\",\"version\": \"v1.30.0\", \"cpes\": [\"cpe:2.3:a:scanoss:scanoss.py:1.30.0:*:*:*:*:*:*:*\"]} ], \"status\": {\"status\": \"SUCCESS\", \"message\": \"CPEs Successfully retrieved\"}}' + _globals['_VULNERABILITYRESPONSE']._loaded_options = None + _globals['_VULNERABILITYRESPONSE']._serialized_options = b'\030\001' + _globals['_COMPONENTVULNERABILITYRESPONSE']._loaded_options = None + _globals['_COMPONENTVULNERABILITYRESPONSE']._serialized_options = b'\222A\266\004\n\263\004J\260\004{\"component\":{\"purl\": \"pkg:github/scanoss/engine\", \"requirement\": \"=>1.0.0\", \"version\": \"1.0.0\", \"vulnerabilities\": [{\"id\": \"DLA-2640-1\", \"cve\": \"DLA-2640-1\", \"url\": \"https://osv.dev/vulnerability/DLA-2640-1\", \"summary\": \"gst-plugins-good1.0 - security update\", \"severity\": \"Critical\", \"published\": \"2021-04-26\", \"modified\": \"2025-05-26\", \"source\": \"OSV\", \"cvss\": [{\"cvss\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\", \"cvss_score\": 9.8, \"cvss_severity\": \"Critical\"}]}]}, \"status\": {\"status\": \"SUCCESS\", \"message\": \"Vulnerabilities Successfully retrieved\"}}' + _globals['_COMPONENTSVULNERABILITYRESPONSE']._loaded_options = None + _globals['_COMPONENTSVULNERABILITYRESPONSE']._serialized_options = b'\222A\216\010\n\213\010J\210\010{\"components\":[{\"purl\": \"pkg:github/scanoss/engine\", \"requirement\": \"1.0.0\", \"version\": \"1.0.0\", \"vulnerabilities\": [{\"id\": \"DLA-2640-1\", \"cve\": \"DLA-2640-1\", \"url\": \"https://osv.dev/vulnerability/DLA-2640-1\", \"summary\": \"gst-plugins-good1.0 - security update\", \"severity\": \"Critical\", \"published\": \"2021-04-26\", \"modified\": \"2025-05-26\", \"source\": \"OSV\", \"cvss\": [{\"cvss\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\", \"cvss_score\": 9.8, \"cvss_severity\": \"Critical\"}]}]}, {\"purl\": \"pkg:github/scanoss/scanoss.py\",\"requirement\": \"v1.30.0\",\"version\": \"v1.30.0\", \"vulnerabilities\": [{\"id\": \"CVE-2024-54321\", \"cve\": \"CVE-2024-54321\", \"url\": \"https://nvd.nist.gov/vuln/detail/CVE-2024-54321\", \"summary\": \"Denial of service vulnerability\", \"severity\": \"Medium\", \"published\": \"2024-01-15\", \"modified\": \"2024-02-01\", \"source\": \"NDV\", \"cvss\": [{\"cvss\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L\", \"cvss_score\": 4.3, \"cvss_severity\": \"Medium\"}]}]}], \"status\": {\"status\": \"SUCCESS\", \"message\": \"Vulnerabilities Successfully retrieved\"}}' + _globals['_VULNERABILITIES'].methods_by_name['Echo']._loaded_options = None + _globals['_VULNERABILITIES'].methods_by_name['Echo']._serialized_options = b'\202\323\344\223\002\035\"\030/v2/vulnerabilities/echo:\001*' + _globals['_VULNERABILITIES'].methods_by_name['GetCpes']._loaded_options = None + _globals['_VULNERABILITIES'].methods_by_name['GetCpes']._serialized_options = b'\210\002\001' + _globals['_VULNERABILITIES'].methods_by_name['GetComponentCpes']._loaded_options = None + _globals['_VULNERABILITIES'].methods_by_name['GetComponentCpes']._serialized_options = b'\202\323\344\223\002$\022\"/v2/vulnerabilities/cpes/component' + _globals['_VULNERABILITIES'].methods_by_name['GetComponentsCpes']._loaded_options = None + _globals['_VULNERABILITIES'].methods_by_name['GetComponentsCpes']._serialized_options = b'\202\323\344\223\002(\"#/v2/vulnerabilities/cpes/components:\001*' + _globals['_VULNERABILITIES'].methods_by_name['GetVulnerabilities']._loaded_options = None + _globals['_VULNERABILITIES'].methods_by_name['GetVulnerabilities']._serialized_options = b'\210\002\001' + _globals['_VULNERABILITIES'].methods_by_name['GetComponentVulnerabilities']._loaded_options = None + _globals['_VULNERABILITIES'].methods_by_name['GetComponentVulnerabilities']._serialized_options = b'\202\323\344\223\002\037\022\035/v2/vulnerabilities/component' + _globals['_VULNERABILITIES'].methods_by_name['GetComponentsVulnerabilities']._loaded_options = None + _globals['_VULNERABILITIES'].methods_by_name['GetComponentsVulnerabilities']._serialized_options = b'\202\323\344\223\002#\"\036/v2/vulnerabilities/components:\001*' + _globals['_VULNERABILITYREQUEST']._serialized_start=219 + _globals['_VULNERABILITYREQUEST']._serialized_end=364 + _globals['_VULNERABILITYREQUEST_PURLS']._serialized_start=318 + _globals['_VULNERABILITYREQUEST_PURLS']._serialized_end=360 + _globals['_CPERESPONSE']._serialized_start=367 + _globals['_CPERESPONSE']._serialized_end=542 + _globals['_CPERESPONSE_PURLS']._serialized_start=503 + _globals['_CPERESPONSE_PURLS']._serialized_end=538 + _globals['_COMPONENTCPESINFO']._serialized_start=544 + _globals['_COMPONENTCPESINFO']._serialized_end=629 + _globals['_COMPONENTCPESRESPONSE']._serialized_start=632 + _globals['_COMPONENTCPESRESPONSE']._serialized_end=1027 + _globals['_COMPONENTSCPESRESPONSE']._serialized_start=1030 + _globals['_COMPONENTSCPESRESPONSE']._serialized_end=1581 + _globals['_CVSS']._serialized_start=1583 + _globals['_CVSS']._serialized_end=1673 + _globals['_VULNERABILITY']._serialized_start=1676 + _globals['_VULNERABILITY']._serialized_end=1869 + _globals['_VULNERABILITYRESPONSE']._serialized_start=1872 + _globals['_VULNERABILITYRESPONSE']._serialized_end=2125 + _globals['_VULNERABILITYRESPONSE_PURLS']._serialized_start=2028 + _globals['_VULNERABILITYRESPONSE_PURLS']._serialized_end=2121 + _globals['_COMPONENTVULNERABILITYINFO']._serialized_start=2128 + _globals['_COMPONENTVULNERABILITYINFO']._serialized_end=2280 + _globals['_COMPONENTVULNERABILITYRESPONSE']._serialized_start=2283 + _globals['_COMPONENTVULNERABILITYRESPONSE']._serialized_end=3022 + _globals['_COMPONENTSVULNERABILITYRESPONSE']._serialized_start=3025 + _globals['_COMPONENTSVULNERABILITYRESPONSE']._serialized_end=4238 + _globals['_VULNERABILITIES']._serialized_start=4241 + _globals['_VULNERABILITIES']._serialized_end=5316 +# @@protoc_insertion_point(module_scope) diff --git a/src/scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py b/src/scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py new file mode 100644 index 00000000..158aaf6b --- /dev/null +++ b/src/scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py @@ -0,0 +1,406 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +from scanoss.api.common.v2 import scanoss_common_pb2 as scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2 +from scanoss.api.vulnerabilities.v2 import scanoss_vulnerabilities_pb2 as scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2 + +GRPC_GENERATED_VERSION = '1.73.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class VulnerabilitiesStub(object): + """ + Vulnerability Service Definition + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Echo = channel.unary_unary( + '/scanoss.api.vulnerabilities.v2.Vulnerabilities/Echo', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, + _registered_method=True) + self.GetCpes = channel.unary_unary( + '/scanoss.api.vulnerabilities.v2.Vulnerabilities/GetCpes', + request_serializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.VulnerabilityRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.CpeResponse.FromString, + _registered_method=True) + self.GetComponentCpes = channel.unary_unary( + '/scanoss.api.vulnerabilities.v2.Vulnerabilities/GetComponentCpes', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.ComponentCpesResponse.FromString, + _registered_method=True) + self.GetComponentsCpes = channel.unary_unary( + '/scanoss.api.vulnerabilities.v2.Vulnerabilities/GetComponentsCpes', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.ComponentsCpesResponse.FromString, + _registered_method=True) + self.GetVulnerabilities = channel.unary_unary( + '/scanoss.api.vulnerabilities.v2.Vulnerabilities/GetVulnerabilities', + request_serializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.VulnerabilityRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.VulnerabilityResponse.FromString, + _registered_method=True) + self.GetComponentVulnerabilities = channel.unary_unary( + '/scanoss.api.vulnerabilities.v2.Vulnerabilities/GetComponentVulnerabilities', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.ComponentVulnerabilityResponse.FromString, + _registered_method=True) + self.GetComponentsVulnerabilities = channel.unary_unary( + '/scanoss.api.vulnerabilities.v2.Vulnerabilities/GetComponentsVulnerabilities', + request_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + response_deserializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.ComponentsVulnerabilityResponse.FromString, + _registered_method=True) + + +class VulnerabilitiesServicer(object): + """ + Vulnerability Service Definition + """ + + def Echo(self, request, context): + """ + Returns the same message that was sent, used for health checks and connectivity testing + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetCpes(self, request, context): + """ + Get CPEs (Common Platform Enumeration) associated with a PURL - legacy endpoint. + + Legacy method for retrieving Common Platform Enumeration identifiers + associated with software components. Use GetComponentCpes instead. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentCpes(self, request, context): + """ + Get CPEs (Common Platform Enumeration) associated with a single software component. + + Returns Common Platform Enumeration identifiers that match the specified component. + CPEs are used to identify IT platforms in vulnerability databases and enable + vulnerability scanning and assessment. + + See: https://github.com/scanoss/papi/blob/main/protobuf/scanoss/api/vulnerabilities/v2/README.md?tab=readme-ov-file#getcomponentcpes + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentsCpes(self, request, context): + """ + Get CPEs (Common Platform Enumeration) associated with multiple software components. + + Returns Common Platform Enumeration identifiers for multiple components in a single request. + CPEs are used to identify IT platforms in vulnerability databases and enable + vulnerability scanning and assessment. + + See: https://github.com/scanoss/papi/blob/main/protobuf/scanoss/api/vulnerabilities/v2/README.md?tab=readme-ov-file#getcomponentscpes + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetVulnerabilities(self, request, context): + """ + Get vulnerability details - legacy endpoint. + + Legacy method for retrieving vulnerability information for software components. + Use GetComponentVulnerabilities or GetComponentsVulnerabilities instead. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentVulnerabilities(self, request, context): + """ + Get vulnerability information for a single software component. + + Analyzes the component and returns known vulnerabilities including CVE details, + severity scores, publication dates, and other security metadata. + Vulnerability data is sourced from various security databases and feeds. + + See: https://github.com/scanoss/papi/blob/main/protobuf/scanoss/api/vulnerabilities/v2/README.md?tab=readme-ov-file#getcomponentvulnerabilities + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetComponentsVulnerabilities(self, request, context): + """ + Get vulnerability information for multiple software components in a single request. + + Analyzes multiple components and returns known vulnerabilities for each including CVE details, + severity scores, publication dates, and other security metadata. + Vulnerability data is sourced from various security databases and feeds. + + See: https://github.com/scanoss/papi/blob/main/protobuf/scanoss/api/vulnerabilities/v2/README.md?tab=readme-ov-file#getcomponentsvulnerabilities + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_VulnerabilitiesServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Echo': grpc.unary_unary_rpc_method_handler( + servicer.Echo, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.FromString, + response_serializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.SerializeToString, + ), + 'GetCpes': grpc.unary_unary_rpc_method_handler( + servicer.GetCpes, + request_deserializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.VulnerabilityRequest.FromString, + response_serializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.CpeResponse.SerializeToString, + ), + 'GetComponentCpes': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentCpes, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.FromString, + response_serializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.ComponentCpesResponse.SerializeToString, + ), + 'GetComponentsCpes': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentsCpes, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.FromString, + response_serializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.ComponentsCpesResponse.SerializeToString, + ), + 'GetVulnerabilities': grpc.unary_unary_rpc_method_handler( + servicer.GetVulnerabilities, + request_deserializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.VulnerabilityRequest.FromString, + response_serializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.VulnerabilityResponse.SerializeToString, + ), + 'GetComponentVulnerabilities': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentVulnerabilities, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.FromString, + response_serializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.ComponentVulnerabilityResponse.SerializeToString, + ), + 'GetComponentsVulnerabilities': grpc.unary_unary_rpc_method_handler( + servicer.GetComponentsVulnerabilities, + request_deserializer=scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.FromString, + response_serializer=scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.ComponentsVulnerabilityResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'scanoss.api.vulnerabilities.v2.Vulnerabilities', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('scanoss.api.vulnerabilities.v2.Vulnerabilities', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class Vulnerabilities(object): + """ + Vulnerability Service Definition + """ + + @staticmethod + def Echo(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.vulnerabilities.v2.Vulnerabilities/Echo', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoRequest.SerializeToString, + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.EchoResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetCpes(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.vulnerabilities.v2.Vulnerabilities/GetCpes', + scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.VulnerabilityRequest.SerializeToString, + scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.CpeResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentCpes(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.vulnerabilities.v2.Vulnerabilities/GetComponentCpes', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.ComponentCpesResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentsCpes(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.vulnerabilities.v2.Vulnerabilities/GetComponentsCpes', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.ComponentsCpesResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetVulnerabilities(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.vulnerabilities.v2.Vulnerabilities/GetVulnerabilities', + scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.VulnerabilityRequest.SerializeToString, + scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.VulnerabilityResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentVulnerabilities(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.vulnerabilities.v2.Vulnerabilities/GetComponentVulnerabilities', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentRequest.SerializeToString, + scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.ComponentVulnerabilityResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetComponentsVulnerabilities(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/scanoss.api.vulnerabilities.v2.Vulnerabilities/GetComponentsVulnerabilities', + scanoss_dot_api_dot_common_dot_v2_dot_scanoss__common__pb2.ComponentsRequest.SerializeToString, + scanoss_dot_api_dot_vulnerabilities_dot_v2_dot_scanoss__vulnerabilities__pb2.ComponentsVulnerabilityResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/src/scanoss/cli.py b/src/scanoss/cli.py index beec052d..af5f36f9 100644 --- a/src/scanoss/cli.py +++ b/src/scanoss/cli.py @@ -1,36 +1,96 @@ -#!/usr/bin/env python3 """ - SPDX-License-Identifier: MIT - - Copyright (c) 2021, SCANOSS - - 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. +SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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. """ + import argparse import os import sys +import traceback +from dataclasses import asdict +from pathlib import Path +from typing import List + +import pypac + +from scanoss.cryptography import Cryptography, create_cryptography_config_from_args +from scanoss.delta import Delta +from scanoss.export.dependency_track import DependencyTrackExporter +from scanoss.scanners.container_scanner import ( + DEFAULT_SYFT_COMMAND, + DEFAULT_SYFT_TIMEOUT, + ContainerScanner, + create_container_scanner_config_from_args, +) +from scanoss.scanners.folder_hasher import ( + FolderHasher, + create_folder_hasher_config_from_args, +) +from scanoss.scanossgrpc import ( + ScanossGrpc, + ScanossGrpcError, + create_grpc_config_from_args, +) -from .scanner import Scanner -from .winnowing import Winnowing +from . import __version__ +from .components import Components +from .constants import ( + DEFAULT_API_TIMEOUT, + DEFAULT_COPYLEFT_LICENSE_SOURCES, + DEFAULT_HFH_DEPTH, + DEFAULT_HFH_MIN_ACCEPTED_SCORE, + DEFAULT_HFH_RANK_THRESHOLD, + DEFAULT_HFH_RECURSIVE_THRESHOLD, + DEFAULT_POST_SIZE, + DEFAULT_RETRY, + DEFAULT_TIMEOUT, + MIN_TIMEOUT, + PYTHON_MAJOR_VERSION, + VALID_LICENSE_SOURCES, +) +from .csvoutput import CsvOutput +from .cyclonedx import CycloneDx +from .filecount import FileCount +from .gitlabqualityreport import GitLabQualityReport +from .inspection.policy_check.dependency_track.project_violation import ( + DependencyTrackProjectViolationPolicyCheck, +) +from .inspection.policy_check.scanoss.copyleft import Copyleft +from .inspection.policy_check.scanoss.undeclared_component import UndeclaredComponent +from .inspection.summary.component_summary import ComponentSummary +from .inspection.summary.license_summary import LicenseSummary +from .inspection.summary.match_summary import MatchSummary +from .results import Results from .scancodedeps import ScancodeDeps +from .scanner import FAST_WINNOWING, Scanner +from .scanners.scanner_config import create_scanner_config_from_args +from .scanners.scanner_hfh import ScannerHFH +from .scanoss_settings import ScanossSettings, ScanossSettingsError from .scantype import ScanType -from . import __version__ +from .spdxlite import SpdxLite +from .threadeddependencies import SCOPE +from .utils.file import validate_json_file + +HEADER_PARTS_COUNT = 2 def print_stderr(*args, **kwargs): @@ -40,110 +100,1317 @@ def print_stderr(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) -def setup_args() -> None: +def setup_args() -> None: # noqa: PLR0912, PLR0915 """ Setup all the command line arguments for processing """ - parser = argparse.ArgumentParser(description=f'SCANOSS Python CLI. Ver: {__version__}, License: MIT') - subparsers = parser.add_subparsers(title='Sub Commands', dest='subparser', description='valid subcommands', - help='sub-command help' - ) + parser = argparse.ArgumentParser( + description=f'SCANOSS Python CLI. Ver: {__version__}, License: MIT, Fast Winnowing: {FAST_WINNOWING}' + ) + parser.add_argument('--version', '-v', action='store_true', help='Display version details') + + subparsers = parser.add_subparsers( + title='Sub Commands', dest='subparser', description='valid subcommands', help='sub-command help' + ) # Sub-command: version - p_ver = subparsers.add_parser('version', aliases=['ver'], - description=f'Version of SCANOSS CLI: {__version__}', help='SCANOSS version') + p_ver = subparsers.add_parser( + 'version', aliases=['ver'], description=f'Version of SCANOSS CLI: {__version__}', help='SCANOSS version' + ) p_ver.set_defaults(func=ver) + # Sub-command: scan - p_scan = subparsers.add_parser('scan', aliases=['sc'], - description=f'Analyse/scan the given source base: {__version__}', - help='Scan source code') + p_scan = subparsers.add_parser( + 'scan', + aliases=['sc'], + description=f'Analyse/scan the given source base: {__version__}', + help='Scan source code', + ) p_scan.set_defaults(func=scan) p_scan.add_argument('scan_dir', metavar='FILE/DIR', type=str, nargs='?', help='A file or folder to scan') - p_scan.add_argument('--wfp', '-w', type=str, - help='Scan a WFP File instead of a folder (optional)' - ) - p_scan.add_argument('--dep', '-p', type=str, - help='Use a dependency file instead of a folder (optional)' - ) - p_scan.add_argument('--identify', '-i', type=str, help='Scan and identify components in SBOM file' ) - p_scan.add_argument('--ignore', '-n', type=str, help='Ignore components specified in the SBOM file' ) - p_scan.add_argument('--output', '-o', type=str, help='Output result file name (optional - default stdout).' ) - p_scan.add_argument('--format', '-f', type=str, choices=['plain', 'cyclonedx', 'spdxlite'], - help='Result output format (optional - default: plain)' - ) - p_scan.add_argument('--threads', '-T', type=int, default=10, - help='Number of threads to use while scanning (optional - default 10)' - ) - p_scan.add_argument('--flags', '-F', type=int, - help='Scanning engine flags (1: disable snippet matching, 2 enable snippet ids, ' - '4: disable dependencies, 8: disable licenses, 16: disable copyrights,' - '32: disable vulnerabilities, 64: disable quality, 128: disable cryptography,' - '256: disable best match, 512: Report identified files)' - ) - p_scan.add_argument('--skip-snippets', '-S', action='store_true', help='Skip the generation of snippets') - p_scan.add_argument('--post-size', '-P', type=int, default=64, - help='Number of kilobytes to limit the post to while scanning (optional - default 64)' - ) - p_scan.add_argument('--timeout', '-M', type=int, default=120, - help='Timeout (in seconds) for API communication (optional - default 120)' - ) - p_scan.add_argument('--no-wfp-output', action='store_true', help='Skip WFP file generation') - p_scan.add_argument('--all-extensions', action='store_true', help='Scan all file extensions') - p_scan.add_argument('--all-folders', action='store_true', help='Scan all folders') - p_scan.add_argument('--all-hidden', action='store_true', help='Scan all hidden files/folders') + p_scan.add_argument('--wfp', '-w', type=str, help='Scan a WFP File instead of a folder (optional)') + p_scan.add_argument('--dep', '-p', type=str, help='Use a dependency file instead of a folder (optional)') + p_scan.add_argument( + '--stdin', '-s', metavar='STDIN-FILENAME', type=str, help='Scan the file contents supplied via STDIN (optional)' + ) + p_scan.add_argument('--files', '-e', type=str, nargs='*', help='List of files to scan.') + p_scan.add_argument('--identify', '-i', type=str, help='Scan and identify components in SBOM file') + p_scan.add_argument('--ignore', '-n', type=str, help='Ignore components specified in the SBOM file') + p_scan.add_argument( + '--threads', '-T', type=int, default=5, help='Number of threads to use while scanning (optional - default 5)' + ) + p_scan.add_argument( + '--flags', + '-F', + type=int, + help='Scanning engine flags (1: disable snippet matching, 2 enable snippet ids, ' + '4: disable dependencies, 8: disable licenses, 16: disable copyrights,' + '32: disable vulnerabilities, 64: disable quality, 128: disable cryptography,' + '256: disable best match only, 512: hide identified files, ' + '1024: enable download_url, 2048: enable GitHub full path, ' + '4096: disable extended server stats)', + ) + p_scan.add_argument( + '--post-size', + '-P', + type=int, + default=DEFAULT_POST_SIZE, + help='Number of kilobytes to limit the post to while scanning (optional - default 32)', + ) + p_scan.add_argument( + '--timeout', + '-M', + type=int, + default=DEFAULT_TIMEOUT, + help='Timeout (in seconds) for API communication (optional - default 180)', + ) + p_scan.add_argument( + '--retry', + '-R', + type=int, + default=DEFAULT_RETRY, + help='Retry limit for API communication (optional - default 5)', + ) p_scan.add_argument('--dependencies', '-D', action='store_true', help='Add Dependency scanning') p_scan.add_argument('--dependencies-only', action='store_true', help='Run Dependency scanning only') - p_scan.add_argument('--sc-command', type=str, help='Scancode command and path if required (optional - default scancode).' ) - p_scan.add_argument('--sc-timeout', type=int, default=600, - help='Timeout (in seconds) for scancode to complete (optional - default 600)' - ) + p_scan.add_argument( + '--sc-command', type=str, help='Scancode command and path if required (optional - default scancode).' + ) + p_scan.add_argument( + '--sc-timeout', + type=int, + default=600, + help='Timeout (in seconds) for scancode to complete (optional - default 600)', + ) + p_scan.add_argument( + '--dep-scope', '-ds', type=SCOPE, help='Filter dependencies by scope - default all (options: dev/prod)' + ) + p_scan.add_argument('--dep-scope-inc', '-dsi', type=str, help='Include dependencies with declared scopes') + p_scan.add_argument('--dep-scope-exc', '-dse', type=str, help='Exclude dependencies with declared scopes') + p_scan.add_argument( + '--no-wfp-output', action='store_true', + help='DEPRECATED: Scans no longer generate scanner_output.wfp. Use "fingerprint -o" to create WFP files.' + ) + p_scan.add_argument( + '--wfp-output', type=str, metavar='FILE', + help='Save fingerprints to specified file during scan' + ) + + # Snippet tuning options + p_scan.add_argument( + '--min-snippet-hits', + type=int, + default=None, + help='Minimum snippet hits required. A value of 0 defers to server configuration (optional)', + ) + p_scan.add_argument( + '--min-snippet-lines', + type=int, + default=None, + help='Minimum snippet lines required. A value of 0 defers to server configuration (optional)', + ) + p_scan.add_argument( + '--ranking', + type=str, + choices=['unset' ,'true', 'false'], + default='unset', + help='Enable or disable ranking (optional - default: server configuration)', + ) + p_scan.add_argument( + '--ranking-threshold', + type=int, + default=-1, + help='Ranking threshold value. Valid range: -1 to 10. A value of -1 defers to server configuration (optional)', + ) + p_scan.add_argument( + '--honour-file-exts', + type=str, + choices=['unset','true', 'false'], + default='unset', + help='Honour file extensions during scanning. When not set, defers to server configuration (optional)', + ) # Sub-command: fingerprint - p_wfp = subparsers.add_parser('fingerprint', aliases=['fp', 'wfp'], - description=f'Fingerprint the given source base: {__version__}', - help='Fingerprint source code') + p_wfp = subparsers.add_parser( + 'fingerprint', + aliases=['fp', 'wfp'], + description=f'Fingerprint the given source base: {__version__}', + help='Fingerprint source code', + ) p_wfp.set_defaults(func=wfp) - p_wfp.add_argument('scan_dir', metavar='FILE/DIR', type=str, nargs='?', - help='A file or folder to scan') - p_wfp.add_argument('--output', '-o', type=str, help='Output result file name (optional - default stdout).' ) + p_wfp.add_argument('scan_dir', metavar='FILE/DIR', type=str, nargs='?', help='A file or folder to scan') + p_wfp.add_argument( + '--stdin', + '-s', + metavar='STDIN-FILENAME', + type=str, + help='Fingerprint the file contents supplied via STDIN (optional)', + ) # Sub-command: dependency - p_dep = subparsers.add_parser('dependencies', aliases=['dp', 'dep'], - description=f'Produce dependency file summary: {__version__}', - help='Scan source code for dependencies') + p_dep = subparsers.add_parser( + 'dependencies', + aliases=['dp', 'dep'], + description=f'Produce dependency file summary: {__version__}', + help='Scan source code for dependencies, but do not decorate them', + ) + p_dep.add_argument('scan_loc', metavar='FILE/DIR', type=str, nargs='?', help='A file or folder to scan') + p_dep.add_argument( + '--container', + type=str, + help='Container image to scan. Supports yourrepo/yourimage:tag, Docker tar, ' + 'OCI tar, OCI directory, SIF Container, or generic filesystem directory.', + ) + p_dep.add_argument( + '--sc-command', type=str, help='Scancode command and path if required (optional - default scancode).' + ) + p_dep.add_argument( + '--sc-timeout', + type=int, + default=600, + help='Timeout (in seconds) for scancode to complete (optional - default 600)', + ) p_dep.set_defaults(func=dependency) - p_dep.add_argument('scan_dir', metavar='FILE/DIR', type=str, nargs='?', help='A file or folder to scan') - p_dep.add_argument('--output', '-o', type=str, help='Output result file name (optional - default stdout).' ) - p_dep.add_argument('--sc-command', type=str, help='Scancode command and path if required (optional - default scancode).' ) - p_dep.add_argument('--sc-timeout', type=int, default=600, - help='Timeout (in seconds) for scancode to complete (optional - default 600)' - ) - - # Global command options - for p in [p_scan]: - p.add_argument('--key', '-k', type=str, - help='SCANOSS API Key token (optional - not required for default OSSKB URL)' - ) - p.add_argument('--apiurl', type=str, - help='SCANOSS API URL (optional - default: https://osskb.org/api/scan/direct)' - ) - p.add_argument('--api2url', type=str, - help='SCANOSS gRPC API 2.0 URL (optional - default: https://api.osskb.org)' - ) - for p in [p_scan, p_wfp, p_dep]: - p.add_argument('--debug', '-d', action='store_true', help='Enable debug messages') + + # Container scan sub-command + p_cs = subparsers.add_parser( + 'container-scan', + aliases=['cs'], + description=f'Analyse/scan the given container image: {__version__}', + help='Scan container image', + ) + p_cs.add_argument( + 'scan_loc', + metavar='IMAGE', + type=str, + nargs='?', + help=( + 'Container image to scan. Supports yourrepo/yourimage:tag, Docker tar, ' + 'OCI tar, OCI directory, SIF Container, or generic filesystem directory.' + ), + ) + p_cs.add_argument( + '--retry', + '-R', + type=int, + default=DEFAULT_RETRY, + help='Retry limit for API communication (optional - default 5)', + ) + p_cs.add_argument( + '--timeout', + '-M', + type=int, + default=DEFAULT_TIMEOUT, + help='Timeout (in seconds) for API communication (optional - default 180)', + ) + p_cs.set_defaults(func=container_scan) + + # Sub-command: file_count + p_fc = subparsers.add_parser( + 'file_count', + aliases=['fc'], + description=f'Produce a file type count summary: {__version__}', + help='Search the source tree and produce a file type summary', + ) + p_fc.set_defaults(func=file_count) + p_fc.add_argument('scan_dir', metavar='DIR', type=str, nargs='?', help='A folder to search') + p_fc.add_argument('--all-hidden', action='store_true', help='Scan all hidden files/folders') + + # Sub-command: convert + p_cnv = subparsers.add_parser( + 'convert', + aliases=['cv', 'cnv', 'cvrt'], + description=f'Convert results files between formats: {__version__}', + help='Convert file format', + ) + p_cnv.set_defaults(func=convert) + p_cnv.add_argument('--input', '-i', type=str, required=True, help='Input file name') + p_cnv.add_argument( + '--format', + '-f', + type=str, + choices=['cyclonedx', 'spdxlite', 'csv', 'glc-codequality'], + default='spdxlite', + help='Output format (optional - default: spdxlite)', + ) + p_cnv.add_argument( + '--input-format', type=str, choices=['plain'], default='plain', help='Input format (optional - default: plain)' + ) + + # Sub-command: component + p_comp = subparsers.add_parser( + 'component', + aliases=['comp'], + description=f'SCANOSS Component commands: {__version__}', + help='Component support commands', + ) + + comp_sub = p_comp.add_subparsers( + title='Component Commands', + dest='subparsercmd', + description='component sub-commands', + help='component sub-commands', + ) + + # Component Sub-command: component vulns + c_vulns = comp_sub.add_parser( + 'vulns', + aliases=['vulnerabilities', 'vu'], + description=f'Show Vulnerability details: {__version__}', + help='Retrieve vulnerabilities for the given components', + ) + c_vulns.set_defaults(func=comp_vulns) + + # Component Sub-command: component licenses + c_licenses = comp_sub.add_parser( + 'licenses', + aliases=['lics'], + description=f'Show License details: {__version__}', + help='Retrieve licenses for the given components', + ) + c_licenses.set_defaults(func=comp_licenses) + + # Component Sub-command: component semgrep + c_semgrep = comp_sub.add_parser( + 'semgrep', + aliases=['sp'], + description=f'Show Semgrep findings: {__version__}', + help='Retrieve semgrep issues/findings for the given components', + ) + c_semgrep.set_defaults(func=comp_semgrep) + + # Component Sub-command: component provenance + c_provenance = comp_sub.add_parser( + 'provenance', + aliases=['prov', 'prv'], + description=f'Show GEO Provenance findings: {__version__}', + help='Retrieve geoprovenance for the given components', + ) + c_provenance.add_argument( + '--origin', + action='store_true', + help='Retrieve geoprovenance using contributors origin (default: declared origin)', + ) + c_provenance.set_defaults(func=comp_provenance) + + # Component Sub-command: component search + c_search = comp_sub.add_parser( + 'search', + aliases=['sc'], + description=f'Search component details: {__version__}', + help='Search for a KB component', + ) + c_search.add_argument('--input', '-i', type=str, help='Input file name') + c_search.add_argument('--search', '-s', type=str, help='Generic component search') + c_search.add_argument('--vendor', '-v', type=str, help='Generic component search') + c_search.add_argument('--comp', '-c', type=str, help='Generic component search') + c_search.add_argument('--package', '-p', type=str, help='Generic component search') + c_search.add_argument('--limit', '-l', type=int, help='Generic component search') + c_search.add_argument('--offset', '-f', type=int, help='Generic component search') + c_search.set_defaults(func=comp_search) + + # Component Sub-command: component versions + c_versions = comp_sub.add_parser( + 'versions', + aliases=['vs'], + description=f'Get component version details: {__version__}', + help='Search for component versions', + ) + c_versions.add_argument('--input', '-i', type=str, help='Input file name') + c_versions.add_argument('--purl', '-p', type=str, help='Generic component search') + c_versions.add_argument('--limit', '-l', type=int, help='Generic component search') + c_versions.set_defaults(func=comp_versions) + + # Component Sub-command: component status + c_status = comp_sub.add_parser( + 'status', + aliases=['sts','st'], + description=f'Show Component Status details: {__version__}', + help='Retrieve development status for the given components', + ) + c_status.set_defaults(func=comp_status) + + # Sub-command: crypto + p_crypto = subparsers.add_parser( + 'crypto', + aliases=['cr'], + description=f'SCANOSS Crypto commands: {__version__}', + help='Crypto support commands', + ) + crypto_sub = p_crypto.add_subparsers( + title='Crypto Commands', + dest='subparsercmd', + description='crypto sub-commands', + help='crypto sub-commands', + ) + + # GetAlgorithms and GetAlgorithmsInRange gRPC APIs + p_crypto_algorithms = crypto_sub.add_parser( + 'algorithms', + aliases=['alg'], + description=f'Show Cryptographic algorithms: {__version__}', + help='Retrieve cryptographic algorithms for the given components', + ) + p_crypto_algorithms.add_argument( + '--with-range', + action='store_true', + help='Returns the list of versions in the specified range that contains cryptographic algorithms', + ) + p_crypto_algorithms.set_defaults(func=crypto_algorithms) + + # GetEncryptionHints and GetHintsInRange gRPC APIs + p_crypto_hints = crypto_sub.add_parser( + 'hints', + description=f'Show Encryption hints: {__version__}', + help='Retrieve encryption hints for the given components', + ) + p_crypto_hints.add_argument( + '--with-range', + action='store_true', + help='Returns the list of versions in the specified range that contains encryption hints', + ) + p_crypto_hints.set_defaults(func=crypto_hints) + + p_crypto_versions_in_range = crypto_sub.add_parser( + 'versions-in-range', + aliases=['vr'], + description=f'Show versions in range: {__version__}', + help="Given a list of PURLS and version ranges, get a list of versions that do/don't contain crypto algorithms", + ) + p_crypto_versions_in_range.set_defaults(func=crypto_versions_in_range) + + # Common purl Component sub-command options + for p in [ + c_vulns, + c_semgrep, + c_provenance, + p_crypto_algorithms, + p_crypto_hints, + p_crypto_versions_in_range, + c_licenses, + c_status, + ]: + p.add_argument('--purl', '-p', type=str, nargs='*', help='Package URL - PURL to process.') + p.add_argument('--input', '-i', type=str, help='Input file name') + + # Common Component sub-command options + for p in [ + c_vulns, + c_search, + c_versions, + c_semgrep, + c_provenance, + p_crypto_algorithms, + p_crypto_hints, + p_crypto_versions_in_range, + c_licenses, + c_status, + ]: + p.add_argument( + '--timeout', + '-M', + type=int, + default=DEFAULT_API_TIMEOUT, + help='Timeout (in seconds) for API communication (optional - default 600)', + ) + + # Common Component sub-command API URL option + for p in [ + c_vulns, + c_search, + c_versions, + c_semgrep, + c_provenance, + c_licenses, + c_status, + ]: + p.add_argument( + '--apiurl', type=str, help='SCANOSS API base URL (optional - default: https://api.osskb.org)' + ) + + # Sub-command: utils + p_util = subparsers.add_parser( + 'utils', + aliases=['ut'], + description=f'SCANOSS Utility commands: {__version__}', + help='General utility support commands', + ) + + utils_sub = p_util.add_subparsers( + title='Utils Commands', dest='subparsercmd', description='utils sub-commands', help='utils sub-commands' + ) + + # Utils Sub-command: utils fast + p_f_f = utils_sub.add_parser( + 'fast', description=f'Is fast winnowing enabled: {__version__}', help='SCANOSS fast winnowing' + ) + p_f_f.set_defaults(func=fast) + + # Utils Sub-command: utils certloc + p_c_loc = utils_sub.add_parser( + 'certloc', + aliases=['cl'], + description=f'Show location of Python CA Certs: {__version__}', + help='Display the location of Python CA Certs', + ) + p_c_loc.set_defaults(func=utils_certloc) + + # Utils Sub-command: utils cert-download + p_c_dwnld = utils_sub.add_parser( + 'cert-download', + aliases=['cdl', 'cert-dl'], + description=f'Download Server SSL Cert: {__version__}', + help="Download the specified server's SSL PEM certificate", + ) + p_c_dwnld.set_defaults(func=utils_cert_download) + p_c_dwnld.add_argument('--hostname', '-n', required=True, type=str, help='Server hostname to download cert from.') + p_c_dwnld.add_argument( + '--port', '-p', required=False, type=int, default=443, help='Server port number (default: 443).' + ) + + # Utils Sub-command: utils pac-proxy + p_p_proxy = utils_sub.add_parser( + 'pac-proxy', + aliases=['pac'], + description=f'Determine Proxy from PAC: {__version__}', + help='Use Proxy Auto-Config to determine proxy configuration', + ) + p_p_proxy.set_defaults(func=utils_pac_proxy) + p_p_proxy.add_argument( + '--pac', + required=False, + type=str, + default='auto', + help='Proxy auto configuration. Specify a file, http url or "auto" to try to discover it.', + ) + p_p_proxy.add_argument( + '--url', + required=False, + type=str, + default='https://api.osskb.org', + help='URL to test (default: https://api.osskb.org).', + ) + + p_results = subparsers.add_parser( + 'results', + aliases=['res'], + description=f'SCANOSS Results commands: {__version__}', + help='Process scan results', + ) + p_results.add_argument( + 'filepath', + metavar='FILEPATH', + type=str, + nargs='?', + help='Path to the file containing the results', + ) + p_results.add_argument( + '--match-type', + '-mt', + help='Filter results by match type (comma-separated, e.g., file,snippet)', + ) + p_results.add_argument( + '--status', + '-s', + help='Filter results by file status (comma-separated, e.g., pending, identified)', + ) + p_results.add_argument( + '--has-pending', + action='store_true', + help='Filter results to only include files with pending status', + ) + p_results.add_argument( + '--output', + '-o', + help='Output result file', + ) + p_results.add_argument( + '--format', + '-f', + choices=['json', 'plain'], + help='Output format (default: plain)', + ) + p_results.set_defaults(func=results) + + # ========================================================================= + # INSPECT SUBCOMMAND - Analysis and validation of scan results + # ========================================================================= + + # Main inspect parser - provides tools for analyzing scan results + p_inspect = subparsers.add_parser( + 'inspect', + aliases=['insp', 'ins'], + description=f'Inspect and analyse scan results: {__version__}', + help='Inspect and analyse scan results', + ) + + # Inspect sub-commands parser + p_inspect_sub = p_inspect.add_subparsers( + title='Inspect Commands', + dest='subparsercmd', + description='Available inspection sub-commands', + help='Choose an inspection type', + ) + + # ------------------------------------------------------------------------- + # RAW RESULTS INSPECTION - Analyse raw scan output + # ------------------------------------------------------------------------- + + # Raw results parser - handles inspection of unprocessed scan results + p_inspect_raw = p_inspect_sub.add_parser( + 'raw', + description='Inspect and analyse SCANOSS raw scan results', + help='Analyse raw scan results for various compliance issues', + ) + + # Raw results sub-commands parser + p_inspect_raw_sub = p_inspect_raw.add_subparsers( + title='Raw Results Inspection Commands', + dest='subparser_subcmd', + description='Tools for analyzing raw scan results', + help='Choose a raw results analysis type', + ) + + # Copyleft license inspection - identifies copyleft license violations + p_inspect_raw_copyleft = p_inspect_raw_sub.add_parser( + 'copyleft', + aliases=['cp'], + description='Identify components with copyleft licenses that may require compliance action', + help='Find copyleft license violations', + ) + + # License summary inspection - provides overview of all detected licenses + p_inspect_raw_license_summary = p_inspect_raw_sub.add_parser( + 'license-summary', + aliases=['lic-summary', 'licsum'], + description='Generate comprehensive summary of all licenses found in scan results', + help='Generate license summary report', + ) + + # Component summary inspection - provides overview of all detected components + p_inspect_raw_component_summary = p_inspect_raw_sub.add_parser( + 'component-summary', + aliases=['comp-summary', 'compsum'], + description='Generate comprehensive summary of all components found in scan results', + help='Generate component summary report', + ) + + # Undeclared components inspection - finds components not declared in SBOM + p_inspect_raw_undeclared = p_inspect_raw_sub.add_parser( + 'undeclared', + aliases=['un'], + description='Identify components present in code but not declared in SBOM files', + help='Find undeclared components', + ) + # SBOM format option for undeclared components inspection + p_inspect_raw_undeclared.add_argument( + '--sbom-format', + required=False, + choices=['legacy', 'settings'], + default='settings', + help='SBOM format type for comparison: legacy or settings (default)', + ) + + # ------------------------------------------------------------------------- + # BACKWARD COMPATIBILITY - Support old inspect command format + # ------------------------------------------------------------------------- + + # Legacy copyleft inspection - backward compatibility for 'scanoss-py inspect copyleft' + p_inspect_legacy_copyleft = p_inspect_sub.add_parser( + 'copyleft', + aliases=['cp'], + description='Identify components with copyleft licenses that may require compliance action', + help='Find copyleft license violations (legacy format)', + ) + + # Legacy undeclared components inspection - backward compatibility for 'scanoss-py inspect undeclared' + p_inspect_legacy_undeclared = p_inspect_sub.add_parser( + 'undeclared', + aliases=['un'], + description='Identify components present in code but not declared in SBOM files', + help='Find undeclared components (legacy format)', + ) + + # SBOM format option for legacy undeclared components inspection + p_inspect_legacy_undeclared.add_argument( + '--sbom-format', + required=False, + choices=['legacy', 'settings'], + default='settings', + help='SBOM format type for comparison: legacy or settings (default)', + ) + + # Legacy license summary inspection - backward compatibility for 'scanoss-py inspect license-summary' + p_inspect_legacy_license_summary = p_inspect_sub.add_parser( + 'license-summary', + aliases=['lic-summary', 'licsum'], + description='Generate comprehensive summary of all licenses found in scan results', + help='Generate license summary report (legacy format)', + ) + + # Legacy component summary inspection - backward compatibility for 'scanoss-py inspect component-summary' + p_inspect_legacy_component_summary = p_inspect_sub.add_parser( + 'component-summary', + aliases=['comp-summary', 'compsum'], + description='Generate comprehensive summary of all components found in scan results', + help='Generate component summary report (legacy format)', + ) + + # Applies the same configuration to both legacy and raw versions + # License filtering options - common to (legacy) copyleft and license summary commands + for p in [ + p_inspect_raw_copyleft, + p_inspect_raw_license_summary, + p_inspect_legacy_copyleft, + p_inspect_legacy_license_summary, + ]: + p.add_argument('--include', help='Additional licenses to include in analysis (comma-separated list)') + p.add_argument('--exclude', help='Licenses to exclude from analysis (comma-separated list)') + p.add_argument('--explicit', help='Use only these specific licenses for analysis (comma-separated list)') + + # License source filtering + for p in [p_inspect_raw_copyleft, p_inspect_legacy_copyleft]: + p.add_argument( + '-ls', '--license-sources', + action='extend', + nargs='+', + choices=VALID_LICENSE_SOURCES, + help=f'Specify which license sources to check for copyleft violations. Each license object in scan results ' + f'has a source field indicating its origin. Default: {", ".join(DEFAULT_COPYLEFT_LICENSE_SOURCES)}', + ) + + # Common options for (legacy) copyleft and undeclared component inspection + for p in [p_inspect_raw_copyleft, p_inspect_raw_undeclared, p_inspect_legacy_copyleft, p_inspect_legacy_undeclared]: + p.add_argument('-i', '--input', required=True, help='Path to scan results file to analyse') + p.add_argument( + '-f', + '--format', + required=False, + choices=['json', 'md', 'jira_md'], + default='json', + help='Output format: json (default), md (Markdown), or jira_md (JIRA Markdown)', + ) + p.add_argument('-o', '--output', type=str, help='Save detailed results to specified file') + p.add_argument('-s', '--status', type=str, help='Save summary status report to Markdown file') + + # Common options for (legacy) license and component summary commands + for p in [ + p_inspect_raw_license_summary, + p_inspect_raw_component_summary, + p_inspect_legacy_license_summary, + p_inspect_legacy_component_summary, + ]: + p.add_argument('-i', '--input', required=True, help='Path to scan results file to analyse') + p.add_argument('-o', '--output', type=str, help='Save summary report to specified file') + + # ------------------------------------------------------------------------- + # DEPENDENCY TRACK INSPECTION - Analyse Dependency Track project data + # ------------------------------------------------------------------------- + + # Dependency Track parser - handles inspection of DT project status and violations + p_dep_track_sub = p_inspect_sub.add_parser( + 'dependency-track', + aliases=['dt'], + description='Inspect and analyse Dependency Track project status and policy violations', + help='Analyse Dependency Track projects', + ) + + # Dependency Track sub-commands parser + p_inspect_dep_track_sub = p_dep_track_sub.add_subparsers( + title='Dependency Track Inspection Commands', + dest='subparser_subcmd', + description='Tools for analysing Dependency Track project data', + help='Choose a Dependency Track analysis type', + ) + + # Project violations inspection - analyses policy violations in DT projects + p_inspect_dt_project_violation = p_inspect_dep_track_sub.add_parser( + 'project-violations', + aliases=['pv'], + description='Analyse policy violations and compliance issues in Dependency Track projects', + help='Inspect project policy violations', + ) + # Dependency Track connection and authentication options + p_inspect_dt_project_violation.add_argument( + '--url', required=True, type=str, help='Dependency Track server base URL (e.g., https://dtrack.example.com)' + ) + p_inspect_dt_project_violation.add_argument( + '--upload-token', + '-ut', + required=False, + type=str, + help='Project-specific upload token for accessing DT project data', + ) + p_inspect_dt_project_violation.add_argument( + '--project-id', '-pid', required=False, type=str, help='Dependency Track project UUID to inspect' + ) + p_inspect_dt_project_violation.add_argument( + '--apikey', '-k', required=True, type=str, help='Dependency Track API key for authentication' + ) + p_inspect_dt_project_violation.add_argument( + '--project-name', '-pn', required=False, type=str, help='Dependency Track project name' + ) + p_inspect_dt_project_violation.add_argument( + '--project-version', '-pv', required=False, type=str, help='Dependency Track project version' + ) + p_inspect_dt_project_violation.add_argument( + '--output', '-o', required=False, type=str, help='Save inspection results to specified file' + ) + p_inspect_dt_project_violation.add_argument( + '--status', required=False, type=str, help='Save summary status report to specified file' + ) + p_inspect_dt_project_violation.add_argument( + '--format', + '-f', + required=False, + choices=['json', 'md', 'jira_md'], + default='json', + help='Output format: json (default), md (Markdown) or jira_md (JIRA Markdown)', + ) + p_inspect_dt_project_violation.add_argument( + '--timeout', + '-M', + required=False, + default=300, + type=float, + help='Timeout (in seconds) for API communication (optional - default 300 sec)', + ) + + # ============================================================================== + # GitLab Integration Parser + # ============================================================================== + # Main parser for GitLab-specific inspection commands and report generation + p_gitlab_sub = p_inspect_sub.add_parser( + 'gitlab', + aliases=['glc'], + description='Generate GitLab-compatible reports from SCANOSS scan results (Markdown summaries)', + help='Generate GitLab integration reports', + ) + + # GitLab sub-commands parser + # Provides access to different GitLab report formats and inspection tools + p_gitlab_sub_parser = p_gitlab_sub.add_subparsers( + title='GitLab Report Types', + dest='subparser_subcmd', + description='Available GitLab report formats for scan result analysis', + help='Select the type of GitLab report to generate', + ) + + # ============================================================================== + # GitLab Matches Summary Command + # ============================================================================== + # Analyzes scan results and generates a GitLab-compatible Markdown summary + p_gl_inspect_matches = p_gitlab_sub_parser.add_parser( + 'matches', + aliases=['ms'], + description='Generate a Markdown summary report of scan matches for GitLab integration', + help='Generate Markdown summary report of scan matches', + ) + + # Input file argument - SCANOSS scan results in JSON format + p_gl_inspect_matches.add_argument( + '-i', '--input', required=True, type=str, help='Path to SCANOSS scan results file (JSON format) to analyze' + ) + + # Line range prefix for GitLab file navigation + # Enables clickable file references in the generated report that link to specific lines in GitLab + p_gl_inspect_matches.add_argument( + '-lpr', + '--line-range-prefix', + required=True, + type=str, + help='Base URL prefix for GitLab file links with line ranges (e.g., https://gitlab.com/org/project/-/blob/main)', + ) + + # Output file argument - where to save the generated Markdown report + p_gl_inspect_matches.add_argument( + '--output', + '-o', + required=False, + type=str, + help='Output file path for the generated Markdown report (default: stdout)', + ) + + # TODO Move to the command call def location + # RAW results + p_inspect_raw_undeclared.set_defaults(func=inspect_undeclared) + p_inspect_raw_copyleft.set_defaults(func=inspect_copyleft) + p_inspect_raw_license_summary.set_defaults(func=inspect_license_summary) + p_inspect_raw_component_summary.set_defaults(func=inspect_component_summary) + # Legacy backward compatibility commands + p_inspect_legacy_copyleft.set_defaults(func=inspect_copyleft) + p_inspect_legacy_undeclared.set_defaults(func=inspect_undeclared) + p_inspect_legacy_license_summary.set_defaults(func=inspect_license_summary) + p_inspect_legacy_component_summary.set_defaults(func=inspect_component_summary) + # Dependency Track + p_inspect_dt_project_violation.set_defaults(func=inspect_dep_track_project_violations) + # GitLab + p_gl_inspect_matches.set_defaults(func=inspect_gitlab_matches) + + # ========================================================================= + # END INSPECT SUBCOMMAND CONFIGURATION + # ========================================================================= + + # Sub-command: export + p_export = subparsers.add_parser( + 'export', + aliases=['exp'], + description=f'Export SBOM files to external platforms: {__version__}', + help='Export SBOM files to external platforms', + ) + + export_sub = p_export.add_subparsers( + title='Export Commands', + dest='subparsercmd', + description='export sub-commands', + help='export sub-commands', + ) + + # Export Sub-command: export dt (Dependency Track) + e_dt = export_sub.add_parser( + 'dt', + aliases=['dependency-track'], + description='Export SBOM to Dependency Track', + help='Upload SBOM files to Dependency Track', + ) + e_dt.add_argument('-i', '--input', type=str, required=True, help='Input SBOM file (CycloneDX JSON format)') + e_dt.add_argument('--url', type=str, required=True, help='Dependency Track base URL') + e_dt.add_argument('--apikey', '-k', type=str, required=True, help='Dependency Track API key') + e_dt.add_argument('--output', '-o', type=str, help='File to save export token and uuid into') + e_dt.add_argument('--project-id', '-pid', type=str, help='Dependency Track project UUID') + e_dt.add_argument('--project-name', '-pn', type=str, help='Dependency Track project name') + e_dt.add_argument('--project-version', '-pv', type=str, help='Dependency Track project version') + e_dt.set_defaults(func=export_dt) + + # Sub-command: folder-scan + p_folder_scan = subparsers.add_parser( + 'folder-scan', + aliases=['fs'], + description=f'Scan the given directory using folder hashing: {__version__}', + help='Scan the given directory using folder hashing', + ) + p_folder_scan.add_argument('scan_dir', metavar='FILE/DIR', type=str, nargs='?', help='The root directory to scan') + p_folder_scan.add_argument( + '--timeout', + '-M', + type=int, + default=600, + help='Timeout (in seconds) for API communication (optional - default 600)', + ) + p_folder_scan.add_argument( + '--format', + '-f', + type=str, + choices=['json', 'cyclonedx', 'raw'], + default='json', + help='Result output format (optional - default: json)', + ) + p_folder_scan.add_argument( + '--rank-threshold', + type=int, + default=DEFAULT_HFH_RANK_THRESHOLD, + help='Filter results to only show those with rank value at or below this threshold (e.g., --rank-threshold 3 ' + 'returns results with rank 1, 2, or 3). Lower rank values indicate higher quality matches.', + ) + p_folder_scan.add_argument( + '--depth', + type=int, + default=DEFAULT_HFH_DEPTH, + help=f'Defines how deep to scan the root directory (optional - default {DEFAULT_HFH_DEPTH})', + ) + p_folder_scan.add_argument( + '--recursive-threshold', + type=float, + default=DEFAULT_HFH_RECURSIVE_THRESHOLD, + help=f'Minimum score threshold to consider a match (optional - default: {DEFAULT_HFH_RECURSIVE_THRESHOLD})', + ) + p_folder_scan.add_argument( + '--min-accepted-score', + type=float, + default=DEFAULT_HFH_MIN_ACCEPTED_SCORE, + help=( + 'Only show results with a score at or above this threshold ' + f'(optional - default: {DEFAULT_HFH_MIN_ACCEPTED_SCORE})' + ), + ) + p_folder_scan.set_defaults(func=folder_hashing_scan) + + # Sub-command: folder-hash + p_folder_hash = subparsers.add_parser( + 'folder-hash', + aliases=['fh'], + description=f'Produce a folder hash for the given directory: {__version__}', + help='Produce a folder hash for the given directory', + ) + p_folder_hash.add_argument('scan_dir', metavar='FILE/DIR', type=str, nargs='?', help='A file or folder to scan') + p_folder_hash.add_argument( + '--format', + '-f', + type=str, + choices=['json'], + default='json', + help='Result output format (optional - default: json)', + ) + p_folder_hash.add_argument( + '--depth', + type=int, + default=DEFAULT_HFH_DEPTH, + help=f'Defines how deep to hash the root directory (optional - default {DEFAULT_HFH_DEPTH})', + ) + p_folder_hash.set_defaults(func=folder_hash) + + # Sub-command: delta + p_delta = subparsers.add_parser( + 'delta', + aliases=['dl'], + description=f'SCANOSS Delta commands: {__version__}', + help='Delta support commands', + ) + + delta_sub = p_delta.add_subparsers( + title='Delta Commands', dest='subparsercmd', description='Delta sub-commands', help='Delta sub-commands' + ) + + # Delta Sub-command: copy + p_copy = delta_sub.add_parser( + 'copy', + aliases=['cp'], + description=f'Copy file list into delta dir: {__version__}', + help='Copy the given list of files into a delta directory', + ) + p_copy.add_argument('--input', '-i', type=str, required=True, help='Input file with diff list') + p_copy.add_argument('--folder', '-fd', type=str, help='Delta folder to copy into') + p_copy.add_argument('--root', '-rd', type=str, help='Root directory to place delta folder') + p_copy.set_defaults(func=delta_copy) + + # Output options + for p in [ + p_scan, + p_cs, + p_wfp, + p_dep, + p_fc, + p_cnv, + c_vulns, + c_search, + c_versions, + c_semgrep, + c_provenance, + p_c_dwnld, + p_folder_scan, + p_folder_hash, + p_crypto_algorithms, + p_crypto_hints, + p_crypto_versions_in_range, + c_licenses, + c_status, + p_copy, + ]: + p.add_argument('--output', '-o', type=str, help='Output result file name (optional - default stdout).') + + # Format options + for p in [p_scan, p_cs]: + choices = ['plain', 'cyclonedx', 'spdxlite', 'csv'] + if p is p_cs: + choices.append('raw') + + p.add_argument( + '--format', + '-f', + type=str, + choices=choices, + default='plain', + help='Result output format (optional - default: plain)', + ) + + # Scanoss settings options + for p in [p_folder_scan, p_scan, p_wfp, p_folder_hash, p_dep]: + p.add_argument( + '--settings', + '-st', + type=str, + help='Settings file to use for scanning (optional - default scanoss.json)', + ) + p.add_argument( + '--skip-settings-file', + '-stf', + action='store_true', + help='Skip default settings file (scanoss.json) if it exists', + ) + + # Global Scan command options + for p in [p_scan, p_cs]: + p.add_argument( + '--apiurl', type=str, help='SCANOSS API base URL (optional - default: https://api.osskb.org)' + ) + + # Global Scan/Fingerprint filter options + for p in [p_scan, p_wfp]: + p.add_argument('--obfuscate', action='store_true', help='Obfuscate fingerprints') + p.add_argument('--all-extensions', action='store_true', help='Fingerprint all file extensions/types...') + p.add_argument('--all-folders', action='store_true', help='Fingerprint all folders...') + p.add_argument('--all-hidden', action='store_true', help='Fingerprint all hidden files/folders...') + p.add_argument('--hpsm', '-H', action='store_true', help='Use High Precision Snippet Matching algorithm.') + p.add_argument('--skip-snippets', '-S', action='store_true', help='Skip the generation of snippets') + p.add_argument('--skip-extension', '-E', type=str, action='append', help='File Extension to skip.') + p.add_argument('--skip-folder', '-O', type=str, action='append', help='Folder to skip.') + p.add_argument( + '--skip-size', + '-Z', + type=int, + default=0, + help='Minimum file size to consider for fingerprinting (optional - default 0 bytes [unlimited])', + ) + p.add_argument('--skip-md5', '-5', type=str, action='append', help='Skip files matching MD5.') + p.add_argument('--strip-hpsm', '-G', type=str, action='append', help='Strip HPSM string from WFP.') + p.add_argument('--strip-snippet', '-N', type=str, action='append', help='Strip Snippet ID string from WFP.') + p.add_argument( + '--skip-headers', + '-skh', + action='store_true', + help='Skip license headers, comments and imports at the beginning of files.', + ) + p.add_argument( + '--skip-headers-limit', + '-shl', + type=int, + default=0, + help='Maximum number of lines to skip when filtering headers (default: 0 = no limit).', + ) + + # Global Scan/GRPC options + for p in [ + p_scan, + c_vulns, + c_search, + c_versions, + c_semgrep, + c_provenance, + p_folder_scan, + p_cs, + p_crypto_algorithms, + p_crypto_hints, + p_crypto_versions_in_range, + c_licenses, + c_status, + ]: + p.add_argument( + '--key', '-k', type=str, help='SCANOSS API Key token (optional - not required for default OSSKB URL)' + ) + p.add_argument( + '--proxy', + type=str, + help='Proxy URL to use for connections (optional). ' + 'Can also use the environment variable "HTTPS_PROXY=:"', + ) + p.add_argument( + '--pac', + type=str, + help='Proxy auto configuration (optional). Specify a file, http url or "auto" to try to discover it.', + ) + p.add_argument( + '--ca-cert', + type=str, + help='Alternative certificate PEM file (optional). ' + 'Can also use the environment variable ' + '"REQUESTS_CA_BUNDLE=/path/to/cacert.pem"', + ) + + # Request options + for p in [ + p_scan, + c_vulns, + c_search, + c_versions, + c_semgrep, + c_provenance, + p_folder_scan, + p_cs, + p_crypto_algorithms, + p_crypto_hints, + p_crypto_versions_in_range, + c_licenses, + c_status, + ]: + p.add_argument( + '--api2url', type=str, + help='REMOVED: gRPC API URL is no longer supported. Using this flag will cause an error.' + ) + p.add_argument( + '--grpc-proxy', type=str, + help='REMOVED: gRPC Proxy URL is no longer supported. Using this flag will cause an error.' + ) + p.add_argument( + '--header', + '-hdr', + action='append', # This allows multiple -H flags + type=str, + help='Headers to be sent on request (e.g., -hdr "Name: Value") - can be used multiple times', + ) + p.add_argument('--ignore-cert-errors', action='store_true', help='Ignore certificate errors') + + # Syft options + for p in [p_cs, p_dep]: + p.add_argument( + '--syft-command', + type=str, + help='Syft command and path if required (optional - default syft).', + default=DEFAULT_SYFT_COMMAND, + ) + p.add_argument( + '--syft-timeout', + type=int, + default=DEFAULT_SYFT_TIMEOUT, + help='Timeout (in seconds) for syft to complete (optional - default 600)', + ) + + # Deprecated gRPC/REST protocol flags (kept for clear error messaging) + for p in [ + c_vulns, + p_scan, + p_cs, + p_crypto_algorithms, + p_crypto_hints, + p_crypto_versions_in_range, + c_semgrep, + c_provenance, + c_search, + c_versions, + c_licenses, + c_status, + p_folder_scan, + ]: + p.add_argument( + '--grpc', action='store_true', + help='REMOVED: gRPC is no longer supported. Using this flag will cause an error.' + ) + p.add_argument( + '--rest', action='store_true', dest='rest', + help='REMOVED: REST is now always used. Using this flag will cause an error.' + ) + + # Help/Trace command options + for p in [ + p_scan, + p_wfp, + p_dep, + p_fc, + p_cnv, + p_c_loc, + p_c_dwnld, + p_p_proxy, + c_vulns, + c_search, + c_versions, + c_semgrep, + p_results, + p_inspect_raw_undeclared, + p_inspect_raw_copyleft, + p_inspect_raw_license_summary, + p_inspect_raw_component_summary, + p_inspect_legacy_copyleft, + p_inspect_legacy_undeclared, + p_inspect_legacy_license_summary, + p_inspect_legacy_component_summary, + p_inspect_dt_project_violation, + p_gl_inspect_matches, + c_provenance, + p_folder_scan, + p_folder_hash, + p_cs, + p_crypto_algorithms, + p_crypto_hints, + p_crypto_versions_in_range, + c_licenses, + c_status, + e_dt, + p_copy, + ]: + p.add_argument( + '--debug', + '-d', + action='store_true', + default=os.environ.get('SCANOSS_DEBUG', '').lower() == 'true', + help='Enable debug messages (can also be set via environment variable SCANOSS_DEBUG)', + ) p.add_argument('--trace', '-t', action='store_true', help='Enable trace messages, including API posts') p.add_argument('--quiet', '-q', action='store_true', help='Enable quiet mode') args = parser.parse_args() + + # Fail on removed gRPC-related flags + _removed_flags_used = [] + if hasattr(args, 'grpc') and args.grpc: + _removed_flags_used.append('--grpc') + if hasattr(args, 'rest') and args.rest: + _removed_flags_used.append('--rest') + if hasattr(args, 'api2url') and args.api2url: + _removed_flags_used.append('--api2url') + if hasattr(args, 'grpc_proxy') and args.grpc_proxy: + _removed_flags_used.append('--grpc-proxy') + if _removed_flags_used: + flags_str = ', '.join(_removed_flags_used) + print_stderr( + f'Error: Removed flag(s) used: {flags_str}. ' + 'gRPC is no longer supported. REST is now used by default. ' + 'Please remove these flags from your command.' + ) + sys.exit(1) + + if args.version: + ver(parser, args) + sys.exit(0) if not args.subparser: - parser.print_help() - exit(1) + parser.print_help() # No sub command subcommand, print general help + sys.exit(1) + elif ( + args.subparser + in ( + 'utils', + 'ut', + 'component', + 'comp', + 'inspect', + 'insp', + 'ins', + 'crypto', + 'cr', + 'export', + 'exp', + 'delta', + 'dl', + ) + ) and not args.subparsercmd: + parser.parse_args([args.subparser, '--help']) # Force utils helps to be displayed + sys.exit(1) + elif ( + (args.subparser in 'inspect') + and (args.subparsercmd in ('raw', 'dt', 'glc', 'gitlab')) + and (args.subparser_subcmd is None) + ): + parser.parse_args([args.subparser, args.subparsercmd, '--help']) # Force utils helps to be displayed + sys.exit(1) args.func(parser, args) # Execute the function associated with the sub-command -def ver(parser, args): +def ver(*_): """ Run the "ver" sub-command + :param _: ignored/unused + """ + print(f'Version: {__version__}') + + +def fast(*_): + """ + Run the "fast" sub-command + :param _: ignored/unused + """ + print(f'Fast Winnowing: {FAST_WINNOWING}') + + +def file_count(parser, args): + """ + Run the "file_count" sub-command Parameters ---------- parser: ArgumentParser @@ -151,7 +1418,28 @@ def ver(parser, args): args: Namespace Parsed arguments """ - print(f'Version: {__version__}') + if not args.scan_dir: + print_stderr('Please specify a folder') + parser.parse_args([args.subparser, '-h']) + sys.exit(1) + if args.output: + initialise_empty_file(args.output) + + counter = FileCount( + debug=args.debug, + quiet=args.quiet, + trace=args.trace, + scan_output=args.output, + hidden_files_folders=args.all_hidden, + ) + if not os.path.exists(args.scan_dir): + print_stderr(f'Error: Folder specified does not exist: {args.scan_dir}.') + sys.exit(1) + if os.path.isdir(args.scan_dir): + counter.count_files(args.scan_dir) + else: + print_stderr(f'Error: Path specified is not a folder: {args.scan_dir}.') + sys.exit(1) def wfp(parser, args): @@ -164,26 +1452,64 @@ def wfp(parser, args): args: Namespace Parsed arguments """ - if not args.scan_dir: - print_stderr('Please specify a file/folder') + if not args.scan_dir and not args.stdin: + print_stderr('Please specify a file/folder or STDIN (--stdin)') parser.parse_args([args.subparser, '-h']) - exit(1) - scan_output: str = None + sys.exit(1) + if args.strip_hpsm and not args.hpsm and not args.quiet: + print_stderr('Warning: --strip-hpsm option supplied without enabling HPSM (--hpsm). Ignoring.') if args.output: - scan_output = args.output - open(scan_output, 'w').close() - scanner = Scanner(debug=args.debug, quiet=args.quiet) + initialise_empty_file(args.output) - if not os.path.exists(args.scan_dir): - print_stderr(f'Error: File or folder specified does not exist: {args.scan_dir}.') - exit(1) - if os.path.isdir(args.scan_dir): - scanner.wfp_folder(args.scan_dir, scan_output) - elif os.path.isfile(args.scan_dir): - scanner.wfp_file(args.scan_dir, scan_output) + # Load scan settings + scanoss_settings = None + if not args.skip_settings_file: + scanoss_settings = ScanossSettings(debug=args.debug, trace=args.trace, quiet=args.quiet) + try: + scanoss_settings.load_json_file(args.settings, args.scan_dir) + except ScanossSettingsError as e: + print_stderr(f'Error: {e}') + sys.exit(1) + + scan_options = 0 if args.skip_snippets else ScanType.SCAN_SNIPPETS.value # Skip snippet generation or not + scanner = Scanner( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + obfuscate=args.obfuscate, + scan_options=scan_options, + all_extensions=args.all_extensions, + all_folders=args.all_folders, + hidden_files_folders=args.all_hidden, + hpsm=args.hpsm, + skip_size=args.skip_size, + skip_extensions=args.skip_extension, + skip_folders=args.skip_folder, + skip_md5_ids=args.skip_md5, + strip_hpsm_ids=args.strip_hpsm, + strip_snippet_ids=args.strip_snippet, + scanoss_settings=scanoss_settings, + skip_headers=args.skip_headers, + skip_headers_limit=args.skip_headers_limit, + ) + if args.stdin: + contents = sys.stdin.buffer.read() + scanner.wfp_contents(args.stdin, contents, args.output) + elif args.scan_dir: + if not os.path.exists(args.scan_dir): + print_stderr(f'Error: File or folder specified does not exist: {args.scan_dir}.') + sys.exit(1) + if os.path.isdir(args.scan_dir): + scanner.wfp_folder(args.scan_dir, args.output) + elif os.path.isfile(args.scan_dir): + scanner.wfp_file(args.scan_dir, args.output) + else: + print_stderr(f'Error: Path specified is neither a file or a folder: {args.scan_dir}.') + sys.exit(1) else: - print_stderr(f'Error: Path specified is neither a file or a folder: {args.scan_dir}.') - exit(1) + print_stderr('No action found to process') + sys.exit(1) + def get_scan_options(args): """ @@ -196,7 +1522,7 @@ def get_scan_options(args): scan_dependencies = 0 if args.skip_snippets: scan_snippets = 0 - if args.dependencies: + if args.dependencies or args.dep: scan_dependencies = ScanType.SCAN_DEPENDENCIES.value if args.dependencies_only: scan_files = scan_snippets = 0 @@ -206,18 +1532,18 @@ def get_scan_options(args): if args.debug: if ScanType.SCAN_FILES.value & scan_options: - print_stderr(f'Scan Files') + print_stderr('Scan Files') if ScanType.SCAN_SNIPPETS.value & scan_options: - print_stderr(f'Scan Snippets') + print_stderr('Scan Snippets') if ScanType.SCAN_DEPENDENCIES.value & scan_options: - print_stderr(f'Scan Dependencies') + print_stderr('Scan Dependencies') if scan_options <= 0: print_stderr(f'Error: No valid scan options configured: {scan_options}') - exit(1) + sys.exit(1) return scan_options -def scan(parser, args): +def scan(parser, args): # noqa: PLR0912, PLR0915 """ Run the "scan" sub-command Parameters @@ -227,97 +1553,198 @@ def scan(parser, args): args: Namespace Parsed arguments """ - if not args.scan_dir and not args.wfp: - print_stderr('Please specify a file/folder or fingerprint (--wfp)') + if not args.scan_dir and not args.wfp and not args.stdin and not args.dep and not args.files: + print_stderr( + 'Please specify a file/folder, files (--files), fingerprint (--wfp), dependency (--dep), or STDIN (--stdin)' + ) parser.parse_args([args.subparser, '-h']) - exit(1) - scan_type: str = None - sbom_path: str = None - if args.identify: - sbom_path = args.identify - scan_type = 'identify' - if not os.path.exists(sbom_path) or not os.path.isfile(sbom_path): - print_stderr(f'Specified --identify file does not exist or is not a file: {sbom_path}') - exit(1) - if not Scanner.valid_json_file(sbom_path): # Make sure it's a valid JSON file - exit(1) - if args.ignore: - print_stderr(f'Warning: Specified --identify and --ignore options. Skipping ignore.') - elif args.ignore: - sbom_path = args.ignore - scan_type = 'blacklist' - if not os.path.exists(sbom_path) or not os.path.isfile(sbom_path): - print_stderr(f'Specified --ignore file does not exist or is not a file: {sbom_path}') - exit(1) - if not Scanner.valid_json_file(sbom_path): # Make sure it's a valid JSON file - exit(1) + sys.exit(1) + if args.no_wfp_output: + print_stderr('Warning: --no-wfp-output is deprecated and has no effect. It will be removed in a future version') + if args.pac and args.proxy: + print_stderr('Please specify one of --proxy or --pac, not both') + parser.parse_args([args.subparser, '-h']) + sys.exit(1) + if args.identify and args.settings: + print_stderr('ERROR: Cannot specify both --identify and --settings options.') + sys.exit(1) + if args.settings and args.skip_settings_file: + print_stderr('ERROR: Cannot specify both --settings and --skip-file-settings options.') + sys.exit(1) + # Figure out which settings (if any) to load before processing + scanoss_settings = None + if not args.skip_settings_file: + scanoss_settings = ScanossSettings(debug=args.debug, trace=args.trace, quiet=args.quiet) + try: + if args.identify: + scanoss_settings.load_json_file(args.identify, args.scan_dir).set_file_type('legacy').set_scan_type( + 'identify' + ) + elif args.ignore: + scanoss_settings.load_json_file(args.ignore, args.scan_dir).set_file_type('legacy').set_scan_type( + 'blacklist' + ) + else: + scanoss_settings.load_json_file(args.settings, args.scan_dir).set_file_type('new') + + except ScanossSettingsError as e: + print_stderr(f'Error: {e}') + sys.exit(1) if args.dep: if not os.path.exists(args.dep) or not os.path.isfile(args.dep): print_stderr(f'Specified --dep file does not exist or is not a file: {args.dep}') - exit(1) - if not Scanner.valid_json_file(args.dep): # Make sure it's a valid JSON file - exit(1) + sys.exit(1) + result = validate_json_file(args.dep) + if not result.is_valid: + print_stderr(f'Error: Dependency file is not valid: {result.error}') + sys.exit(1) + if args.strip_hpsm and not args.hpsm and not args.quiet: + print_stderr('Warning: --strip-hpsm option supplied without enabling HPSM (--hpsm). Ignoring.') - scan_output: str = None if args.output: - scan_output = args.output - open(scan_output, 'w').close() + initialise_empty_file(args.output) output_format = args.format if args.format else 'plain' flags = args.flags if args.flags else None if args.debug and not args.quiet: + if args.skip_settings_file: + print_stderr('Skipping Settings file...') if args.all_extensions: - print_stderr("Scanning all file extensions/types...") + print_stderr('Scanning all file extensions/types...') if args.all_folders: - print_stderr("Scanning all folders...") + print_stderr('Scanning all folders...') if args.all_hidden: - print_stderr("Scanning all hidden files/folders...") + print_stderr('Scanning all hidden files/folders...') if args.skip_snippets: - print_stderr("Skipping snippets...") - if args.post_size != 64: + print_stderr('Skipping snippets...') + if args.post_size != DEFAULT_POST_SIZE: print_stderr(f'Changing scanning POST size to: {args.post_size}k...') - if args.timeout != 120: + if args.timeout != DEFAULT_TIMEOUT: print_stderr(f'Changing scanning POST timeout to: {args.timeout}...') + if args.retry != DEFAULT_RETRY: + print_stderr(f'Changing scanning POST retry to: {args.retry}...') + if args.obfuscate: + print_stderr('Obfuscating file fingerprints...') + if args.proxy: + print_stderr(f'Using Proxy {args.proxy}...') + if args.pac: + print_stderr(f'Using Proxy Auto-config (PAC) {args.pac}...') + if args.ca_cert: + print_stderr(f'Using Certificate {args.ca_cert}...') + if args.hpsm: + print_stderr('Setting HPSM mode...') + if flags: + print_stderr(f'Using flags {flags}...') elif not args.quiet: - if args.timeout < 5: + if args.timeout < MIN_TIMEOUT: print_stderr(f'POST timeout (--timeout) too small: {args.timeout}. Reverting to default.') + if args.retry < 0: + print_stderr(f'POST retry (--retry) too small: {args.retry}. Reverting to default.') - if not os.access( os.getcwd(), os.W_OK ): # Make sure the current directory is writable. If not disable saving WFP - print_stderr(f'Warning: Current directory is not writable: {os.getcwd()}') - args.no_wfp_output = True - - scan_options = get_scan_options(args) # Figure out what scanning options we have + if args.ca_cert and not os.path.exists(args.ca_cert): + print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.') + sys.exit(1) + pac_file = get_pac_file(args.pac) + scan_options = get_scan_options(args) # Figure out what scanning options we have - scanner = Scanner(debug=args.debug, trace=args.trace, quiet=args.quiet, api_key=args.key, url=args.apiurl, - sbom_path=sbom_path, scan_type=scan_type, scan_output=scan_output, output_format=output_format, - flags=flags, nb_threads=args.threads, post_size=args.post_size, - timeout=args.timeout, no_wfp_file=args.no_wfp_output, all_extensions=args.all_extensions, - all_folders=args.all_folders, hidden_files_folders=args.all_hidden, - scan_options=scan_options, sc_timeout=args.sc_timeout, sc_command=args.sc_command, grpc_url=args.api2url - ) + scanner = Scanner( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + api_key=args.key, + url=args.apiurl, + scan_output=args.output, + output_format=output_format, + flags=flags, + nb_threads=args.threads, + post_size=args.post_size, + timeout=args.timeout, + all_extensions=args.all_extensions, + all_folders=args.all_folders, + hidden_files_folders=args.all_hidden, + scan_options=scan_options, + sc_timeout=args.sc_timeout, + sc_command=args.sc_command, + obfuscate=args.obfuscate, + ignore_cert_errors=args.ignore_cert_errors, + proxy=args.proxy, + pac=pac_file, + ca_cert=args.ca_cert, + retry=args.retry, + hpsm=args.hpsm, + skip_size=args.skip_size, + skip_extensions=args.skip_extension, + skip_folders=args.skip_folder, + skip_md5_ids=args.skip_md5, + strip_hpsm_ids=args.strip_hpsm, + strip_snippet_ids=args.strip_snippet, + scanoss_settings=scanoss_settings, + req_headers=process_req_headers(args.header), + min_snippet_hits=args.min_snippet_hits, + min_snippet_lines=args.min_snippet_lines, + ranking=args.ranking, + ranking_threshold=args.ranking_threshold, + honour_file_exts=args.honour_file_exts, + skip_headers=args.skip_headers, + skip_headers_limit=args.skip_headers_limit, + wfp_output=args.wfp_output, + ) if args.wfp: if not scanner.is_file_or_snippet_scan(): print_stderr(f'Error: Cannot specify WFP scanning if file/snippet options are disabled ({scan_options})') - exit(1) - if args.threads > 1: - scanner.scan_wfp_file_threaded(args.wfp) - else: - scanner.scan_wfp_file(args.wfp) + sys.exit(1) + if scanner.is_dependency_scan() and not args.dep: + print_stderr('Error: Cannot specify WFP & Dependency scanning without a dependency file (--dep)') + sys.exit(1) + scanner.scan_wfp_with_options(args.wfp, args.dep) + elif args.stdin: + contents = sys.stdin.buffer.read() + if not scanner.scan_contents(args.stdin, contents): + sys.exit(1) + elif args.files: + if not scanner.scan_files_with_options(args.files, args.dep, scanner.winnowing.file_map): + sys.exit(1) elif args.scan_dir: if not os.path.exists(args.scan_dir): print_stderr(f'Error: File or folder specified does not exist: {args.scan_dir}.') - exit(1) + sys.exit(1) if os.path.isdir(args.scan_dir): - if not scanner.scan_folder_with_options(args.scan_dir): - exit(1) + if not scanner.scan_folder_with_options( + args.scan_dir, + args.dep, + scanner.winnowing.file_map, + args.dep_scope, + args.dep_scope_inc, + args.dep_scope_exc, + ): + sys.exit(1) elif os.path.isfile(args.scan_dir): - if not scanner.scan_file_with_options(args.scan_dir): - exit(1) + if not scanner.scan_file_with_options( + args.scan_dir, + args.dep, + scanner.winnowing.file_map, + args.dep_scope, + args.dep_scope_inc, + args.dep_scope_exc, + ): + sys.exit(1) else: print_stderr(f'Error: Path specified is neither a file or a folder: {args.scan_dir}.') - exit(1) + sys.exit(1) + elif args.dep: + if not args.dependencies_only: + print_stderr( + 'Error: No file or folder specified to scan.' + ' Please add --dependencies-only to decorate dependency file only.' + ) + sys.exit(1) + if not scanner.scan_folder_with_options( + '.', args.dep, scanner.winnowing.file_map, args.dep_scope, args.dep_scope_inc, args.dep_scope_exc + ): + sys.exit(1) else: print_stderr('No action found to process') - exit(1) + sys.exit(1) + def dependency(parser, args): """ @@ -329,23 +1756,1212 @@ def dependency(parser, args): args: Namespace Parsed arguments """ - if not args.scan_dir: - print_stderr('Please specify a file/folder') + if not args.scan_loc and not args.container: + print_stderr('Please specify a file/folder or container image') parser.parse_args([args.subparser, '-h']) - exit(1) - if not os.path.exists(args.scan_dir): - print_stderr(f'Error: File or folder specified does not exist: {args.scan_dir}.') - exit(1) - scan_output: str = None + sys.exit(1) + + # Workaround to return syft scan results converted to our dependency output format + if args.container: + args.scan_loc = args.container + return container_scan(parser, args, only_interim_results=True) + + if not os.path.exists(args.scan_loc): + print_stderr(f'Error: File or folder specified does not exist: {args.scan_loc}.') + sys.exit(1) + if args.output: + initialise_empty_file(args.output) + + if args.settings and args.skip_settings_file: + print_stderr('ERROR: Cannot specify both --settings and --skip-file-settings options.') + sys.exit(1) + scanoss_settings = None + if not args.skip_settings_file: + scanoss_settings = ScanossSettings(debug=args.debug, trace=args.trace, quiet=args.quiet) + try: + scanoss_settings.load_json_file(args.settings, args.scan_loc) + except ScanossSettingsError as e: + print_stderr(f'Error: {e}') + sys.exit(1) + + sc_deps = ScancodeDeps( + debug=args.debug, quiet=args.quiet, trace=args.trace, sc_command=args.sc_command, timeout=args.sc_timeout, + scanoss_settings=scanoss_settings, + ) + if not sc_deps.get_dependencies( + what_to_scan=args.scan_loc, result_output=args.output + ): + sys.exit(1) + return None + + +def convert(parser, args): + """ + Run the "convert" sub-command + Parameters + ---------- + parser: ArgumentParser + command line parser object + args: Namespace + Parsed arguments + """ + if not args.input: + print_stderr('Please specify an input file to convert') + parser.parse_args([args.subparser, '-h']) + sys.exit(1) + success = False + if args.format == 'cyclonedx': + if not args.quiet: + print_stderr('Producing CycloneDX report...') + cdx = CycloneDx(debug=args.debug, output_file=args.output) + success = cdx.produce_from_file(args.input) + elif args.format == 'spdxlite': + if not args.quiet: + print_stderr('Producing SPDX Lite report...') + spdxlite = SpdxLite(debug=args.debug, output_file=args.output) + success = spdxlite.produce_from_file(args.input) + elif args.format == 'csv': + if not args.quiet: + print_stderr('Producing CSV report...') + csvo = CsvOutput(debug=args.debug, output_file=args.output) + success = csvo.produce_from_file(args.input) + elif args.format == 'glc-codequality': + if not args.quiet: + print_stderr('Producing GitLab code quality report...') + glc_code_quality = GitLabQualityReport(debug=args.debug, trace=args.trace, quiet=args.quiet) + success = glc_code_quality.produce_from_file(args.input, output_file=args.output) + else: + print_stderr(f'ERROR: Unknown output format (--format): {args.format}') + if not success: + sys.exit(1) + + +# ============================================================================= +# INSPECT COMMAND HANDLERS - Functions that execute inspection operations +# ============================================================================= + + +def inspect_copyleft(parser, args): + """ + Handle copyleft license inspection command. + + Analyses scan results to identify components using copyleft licenses + that may require compliance actions such as source code disclosure. + + Parameters + ---------- + parser : ArgumentParser + Command line parser object for help display + args : Namespace + Parsed command line arguments containing: + - input: Path to scan results file + - output: Optional output file path + - status: Optional status summary file path + - format: Output format (json, md, jira_md) + - include/exclude/explicit: License filter options + """ + # Initialise output file if specified + if args.output: + initialise_empty_file(args.output) + # Initialise status summary file if specified + if args.status: + initialise_empty_file(args.status) + try: + # Create and configure copyleft inspector + i_copyleft = Copyleft( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + filepath=args.input, + format_type=args.format, + status=args.status, + output=args.output, + include=args.include, # Additional licenses to check + exclude=args.exclude, # Licenses to ignore + explicit=args.explicit, # Explicit license list + license_sources=args.license_sources, # License sources to check (list) + ) + # Execute inspection and exit with appropriate status code + status, _ = i_copyleft.run() + sys.exit(status) + except Exception as e: + print_stderr(e) + if args.debug: + traceback.print_exc() + sys.exit(1) + + +def inspect_undeclared(parser, args): + """ + Handle undeclared components inspection command. + + Analyses scan results to identify components that are present in the + codebase but not declared in SBOM or manifest files, which may indicate + security or compliance risks. + + Parameters + ---------- + parser : ArgumentParser + Command line parser object for help display + args : Namespace + Parsed command line arguments containing: + - input: Path to scan results file + - output: Optional output file path + - status: Optional status summary file path + - format: Output format (json, md, jira_md) + - sbom_format: SBOM format type (legacy, settings) + """ + # Initialise output file if specified + if args.output: + initialise_empty_file(args.output) + + # Initialise status summary file if specified + if args.status: + initialise_empty_file(args.status) + + try: + # Create and configure undeclared component inspector + i_undeclared = UndeclaredComponent( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + filepath=args.input, + format_type=args.format, + status=args.status, + output=args.output, + sbom_format=args.sbom_format, # Format for SBOM comparison + ) + + # Execute inspection and exit with appropriate status code + status, _ = i_undeclared.run() + sys.exit(status) + except Exception as e: + print_stderr(e) + if args.debug: + traceback.print_exc() + sys.exit(1) + + +def inspect_license_summary(parser, args): + """ + Handle license summary inspection command. + + Generates comprehensive summary of all licenses detected in scan results, + including license counts, risk levels, and compliance recommendations. + + Parameters + ---------- + parser : ArgumentParser + Command line parser object for help display + args : Namespace + Parsed command line arguments containing: + - input: Path to scan results file + - output: Optional output file path + - include/exclude/explicit: License filter options + """ + # Initialise output file if specified + if args.output: + initialise_empty_file(args.output) + + # Create and configure license summary generator + i_license_summary = LicenseSummary( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + filepath=args.input, + output=args.output, + include=args.include, # Additional licenses to include + exclude=args.exclude, # Licenses to exclude from summary + explicit=args.explicit, # Explicit license list to summarize + ) + try: + # Execute summary generation + i_license_summary.run() + except Exception as e: + print_stderr(e) + if args.debug: + traceback.print_exc() + sys.exit(1) + + +def inspect_component_summary(parser, args): + """ + Handle component summary inspection command. + + Generates a comprehensive summary of all components detected in scan results, + including component counts, versions, match types, and security information. + + Parameters + ---------- + parser : ArgumentParser + Command line parser object for help display + args : Namespace + Parsed command line arguments containing: + - input: Path to scan results file + - output: Optional output file path + """ + # Initialise an output file if specified + if args.output: + initialise_empty_file(args.output) # Create/clear output file + + # Create and configure component summary generator + i_component_summary = ComponentSummary( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + filepath=args.input, + output=args.output, + ) + + try: + # Execute summary generation + i_component_summary.run() + except Exception as e: + print_stderr(e) + if args.debug: + traceback.print_exc() + sys.exit(1) + + +def inspect_dep_track_project_violations(parser, args): + """ + Handle Dependency Track project inspection command. + + Analyses Dependency Track projects for policy violations, security issues, + and compliance status. Connects to DT API to retrieve project data and + generate detailed violation reports. + + Parameters + ---------- + parser : ArgumentParser + Command line parser object for help display + args : Namespace + Parsed command line arguments containing: + - url: Dependency Track base URL + - apikey: API key for authentication + - project_id: Project UUID to inspect + - project_name: Project name to inspect + - project_version: Project version to inspect + - upload_token: Upload token for project access + - output: Optional output file path + - format: Output format (json, md) + - timeout: Optional timeout for API requests + + """ + # Make sure we have project id/project name and version + _dt_args_validator(parser, args) + # Initialise the output file if specified + if args.output: + initialise_empty_file(args.output) + # Create and configure Dependency Track inspector + try: + dt_proj_violations = DependencyTrackProjectViolationPolicyCheck( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + output=args.output, + status=args.status, + format_type=args.format, + url=args.url, # DT server URL + api_key=args.apikey, # Authentication key + project_id=args.project_id, # Target project UUID + upload_token=args.upload_token, # Upload access token + project_name=args.project_name, # DT project name + project_version=args.project_version, # DT project version + timeout=args.timeout, + ) + # Execute inspection and exit with appropriate status code + status = dt_proj_violations.run() + sys.exit(status) + except Exception as e: + print_stderr(e) + if args.debug: + traceback.print_exc() + sys.exit(1) + + +def inspect_gitlab_matches(parser, args): + """ + Handle GitLab matches the summary inspection command. + + Analyzes SCANOSS scan results and generates a GitLab-compatible Markdown summary + report of component matches. The report includes match details, file locations, + and optionally clickable links to source files in GitLab repositories. + + This command processes SCANOSS scan output and creates human-readable Markdown. + + Parameters + ---------- + parser : ArgumentParser + Command line parser object for help display + args : Namespace + Parsed command line arguments containing: + - input: Path to SCANOSS scan results file (JSON format) to analyze + - line_range_prefix: Base URL prefix for generating GitLab file links with line ranges + (e.g., 'https://gitlab.com/org/project/-/blob/main') + - output: Optional output file path for the generated Markdown report (default: stdout) + - debug: Enable debug output for troubleshooting + - trace: Enable trace-level logging + - quiet: Suppress informational messages + + Notes + ----- + - The output is formatted in Markdown for optimal display in GitLab + - Line range prefix enables clickable file references in the report + - If output is not specified, the report is written to stdout + """ + + if args.input is None: + parser.parse_args([args.subparser, '-h']) + sys.exit(1) + + if args.line_range_prefix is None: + parser.parse_args([args.subparser, '-h']) + sys.exit(1) + + # Initialize output file if specified (create/truncate) + if args.output: + initialise_empty_file(args.output) + + try: + # Create GitLab matches summary generator with configuration + match_summary = MatchSummary( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + scanoss_results_path=args.input, # Path to SCANOSS JSON results + output=args.output, # Output file path or None for stdout + line_range_prefix=args.line_range_prefix, # GitLab URL prefix for file links + ) + + # Execute the summary generation + match_summary.run() + except Exception as e: + # Handle any errors during report generation + print_stderr(e) + if args.debug: + traceback.print_exc() + sys.exit(1) + + +# ============================================================================= +# END INSPECT COMMAND HANDLERS +# ============================================================================= + + +def export_dt(parser, args): + """ + Validates and exports a Software Bill of Materials (SBOM) to a Dependency-Track server. + + Parameters: + parser (argparse.ArgumentParser): The argument parser to validate input arguments. + args (argparse.Namespace): Parsed arguments passed to the command. + + Raises: + SystemExit: If argument validation fails or uploading the SBOM to the Dependency-Track server + is unsuccessful. + """ + # Make sure we have project id/project name and version + _dt_args_validator(parser, args) + if args.output: + initialise_empty_file(args.output) + if not args.quiet: + print_stderr(f'Outputting export data result to: {args.output}') + try: + dt_exporter = DependencyTrackExporter( + url=args.url, + apikey=args.apikey, + output=args.output, + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + ) + success = dt_exporter.upload_sbom_file( + args.input, args.project_id, args.project_name, args.project_version, args.output + ) + if not success: + sys.exit(1) + except Exception as e: + print_stderr(f'ERROR: {e}') + if args.debug: + traceback.print_exc() + sys.exit(1) + + +def _dt_args_validator(parser, args): + """ + Validates command-line arguments related to project identification. + + Parameters + ---------- + parser : argparse.ArgumentParser + An argument parser instance for handling command-line arguments. + args : argparse.Namespace + Parsed arguments from the command line containing project-related information. + + Raises + ------ + SystemExit + If neither a project ID nor the required combination of project name and + project version is provided, or if any of the compulsory arguments + are missing. + """ + if not args.project_id and not args.project_name and not args.project_version: + print_stderr( + 'Please specify either a project ID (--project-id) or a project name (--project-name) and ' + 'version (--project-version)' + ) + parser.parse_args([args.subparser, '-h']) + sys.exit(1) + if not args.project_id and (not args.project_name or not args.project_version): + print_stderr('Please supply a project name (--project-name) and version (--project-version)') + sys.exit(1) + + +def utils_certloc(*_): + """ + Run the "utils certloc" sub-command + :param _: ignored/unused + """ + import certifi # noqa: PLC0415,I001 + + print(f'CA Cert File: {certifi.where()}') + + +def utils_cert_download(_, args): # pylint: disable=PLR0912 # noqa: PLR0912 + """ + Run the "utils cert-download" sub-command + :param _: ignore/unused + :param args: Parsed arguments + """ + import socket # noqa: PLC0415,I001 + import traceback # noqa: PLC0415,I001 + from urllib.parse import urlparse # noqa: PLC0415,I001 + + from OpenSSL import SSL, crypto # noqa: PLC0415,I001 + + file = sys.stdout + if args.output: + file = open(args.output, 'w') + parsed_url = urlparse(args.hostname) + hostname = parsed_url.hostname or args.hostname # Use the parse hostname, or it None use the supplied one + port = int(parsed_url.port or args.port) # Use the parsed port, if not use the supplied one (default 443) + try: + if args.debug: + print_stderr(f'Connecting to {hostname} on {port}...') + conn = SSL.Connection(SSL.Context(SSL.TLSv1_2_METHOD), socket.socket()) + conn.connect((hostname, port)) + conn.do_handshake() + certs = conn.get_peer_cert_chain() + for index, cert in enumerate(certs): + cert_components = dict(cert.get_subject().get_components()) + if sys.version_info[0] >= PYTHON_MAJOR_VERSION: + cn = cert_components.get(b'CN') + else: + # Fallback for Python versions less than PYTHON_MAJOR_VERSION + cn = cert_components.get('CN') + if not args.quiet: + print_stderr(f'Certificate {index} - CN: {cn}') + if sys.version_info[0] >= PYTHON_MAJOR_VERSION: + print( + (crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf-8')).strip(), file=file + ) # Print the downloaded PEM certificate + else: + print((crypto.dump_certificate(crypto.FILETYPE_PEM, cert)).strip(), file=file) + except SSL.Error as e: + print_stderr(f'ERROR: Exception ({e.__class__.__name__}) Downloading certificate from {hostname}:{port} - {e}.') + if args.debug: + traceback.print_exc() + sys.exit(1) + else: + if args.output: + if args.debug: + print_stderr(f'Saved certificate to {args.output}') + file.close() + + +def utils_pac_proxy(_, args): + """ + Run the "utils pac-proxy" sub-command + :param _: ignore/unused + :param args: Parsed arguments + """ + from pypac.resolver import ProxyResolver # noqa: PLC0415,I001 + + if not args.pac: + print_stderr('Error: No pac file option specified.') + sys.exit(1) + pac_file = get_pac_file(args.pac) + if pac_file is None: + print_stderr(f'No proxy configuration for: {args.pac}') + sys.exit(1) + resolver = ProxyResolver(pac_file) + proxies = resolver.get_proxy_for_requests(args.url) + print(f'Proxies: {proxies}\n') + + +def get_pac_file(pac: str): + """ + Get a PAC file if requested. Load the system version (auto), specific local file, or download URL + :param pac: PAC file (auto, file://..., http...) + :return: PAC File object or None + """ + pac_file = None + if pac: + if pac == 'auto': + pac_file = pypac.get_pac() # try to determine the PAC file + elif pac.startswith('file://'): + pac_local = pac[7:] # Remove 'file://' prefix (7 characters) + if not os.path.exists(pac_local): + print_stderr(f'Error: PAC file does not exist: {pac_local}.') + sys.exit(1) + with open(pac_local) as pf: + pac_file = pypac.get_pac(js=pf.read()) + elif pac.startswith('http'): + pac_file = pypac.get_pac(url=pac) + else: + print_stderr(f'Error: Unknown PAC file option: {pac}. Should be one of "auto", "file://", "https://"') + sys.exit(1) + return pac_file + + +def crypto_algorithms(parser, args): + """ + Run the "crypto algorithms" sub-command + Parameters + ---------- + parser: ArgumentParser + command line parser object + args: Namespace + Parsed arguments + """ + if (not args.purl and not args.input) or (args.purl and args.input): + print_stderr('Please specify an input file or purl to decorate (--purl or --input)') + parser.parse_args([args.subparser, args.subparsercmd, '-h']) + sys.exit(1) + if args.ca_cert and not os.path.exists(args.ca_cert): + print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.') + sys.exit(1) + + try: + config = create_cryptography_config_from_args(args) + grpc_config = create_grpc_config_from_args(args) + if args.pac: + grpc_config.pac = get_pac_file(args.pac) + if args.header: + grpc_config.req_headers = process_req_headers(args.header) + client = ScanossGrpc(**asdict(grpc_config)) + + cryptography = Cryptography(config=config, client=client) + cryptography.get_algorithms() + cryptography.present(output_file=args.output) + except ScanossGrpcError as e: + print_stderr(f'API ERROR: {e}') + sys.exit(1) + except Exception as e: + if args.debug: + import traceback # noqa: PLC0415,I001 + + traceback.print_exc() + print_stderr(f'ERROR: {e}') + sys.exit(1) + + +def crypto_hints(parser, args): + """ + Run the "crypto hints" sub-command + Parameters + ---------- + parser: ArgumentParser + command line parser object + args: Namespace + Parsed arguments + """ + if (not args.purl and not args.input) or (args.purl and args.input): + print_stderr('Please specify an input file or purl to decorate (--purl or --input)') + parser.parse_args([args.subparser, args.subparsercmd, '-h']) + sys.exit(1) + if args.ca_cert and not os.path.exists(args.ca_cert): + print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.') + sys.exit(1) + + try: + config = create_cryptography_config_from_args(args) + grpc_config = create_grpc_config_from_args(args) + if args.pac: + grpc_config.pac = get_pac_file(args.pac) + if args.header: + grpc_config.req_headers = process_req_headers(args.header) + client = ScanossGrpc(**asdict(grpc_config)) + + cryptography = Cryptography(config=config, client=client) + cryptography.get_encryption_hints() + cryptography.present(output_file=args.output) + except ScanossGrpcError as e: + print_stderr(f'API ERROR: {e}') + sys.exit(1) + except Exception as e: + if args.debug: + import traceback # noqa: PLC0415,I001 + + traceback.print_exc() + print_stderr(f'ERROR: {e}') + sys.exit(1) + + +def crypto_versions_in_range(parser, args): + """ + Run the "crypto versions-in-range" sub-command + Parameters + ---------- + parser: ArgumentParser + command line parser object + args: Namespace + Parsed arguments + """ + if (not args.purl and not args.input) or (args.purl and args.input): + print_stderr('Please specify an input file or purl to decorate (--purl or --input)') + parser.parse_args([args.subparser, args.subparsercmd, '-h']) + sys.exit(1) + if args.ca_cert and not os.path.exists(args.ca_cert): + print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.') + sys.exit(1) + + try: + config = create_cryptography_config_from_args(args) + grpc_config = create_grpc_config_from_args(args) + if args.pac: + grpc_config.pac = get_pac_file(args.pac) + if args.header: + grpc_config.req_headers = process_req_headers(args.header) + client = ScanossGrpc(**asdict(grpc_config)) + + cryptography = Cryptography(config=config, client=client) + cryptography.get_versions_in_range() + cryptography.present(output_file=args.output) + except ScanossGrpcError as e: + print_stderr(f'API ERROR: {e}') + sys.exit(1) + except Exception as e: + if args.debug: + import traceback # noqa: PLC0415,I001 + + traceback.print_exc() + print_stderr(f'ERROR: {e}') + sys.exit(1) + + +def comp_vulns(parser, args): + """ + Run the "component vulns" sub-command + Parameters + ---------- + parser: ArgumentParser + command line parser object + args: Namespace + Parsed arguments + """ + if (not args.purl and not args.input) or (args.purl and args.input): + print_stderr('Please specify an input file or purl to decorate (--purl or --input)') + parser.parse_args([args.subparser, args.subparsercmd, '-h']) + sys.exit(1) + if args.ca_cert and not os.path.exists(args.ca_cert): + print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.') + sys.exit(1) + pac_file = get_pac_file(args.pac) + comps = Components( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + grpc_url=args.apiurl, # Legacy param name; accepts the REST API base URL. TODO: rename to url + api_key=args.key, + ca_cert=args.ca_cert, + proxy=args.proxy, + pac=pac_file, + timeout=args.timeout, + req_headers=process_req_headers(args.header), + ignore_cert_errors=args.ignore_cert_errors, + ) + if not comps.get_vulnerabilities(args.input, args.purl, args.output): + sys.exit(1) + + +def comp_semgrep(parser, args): + """ + Run the "component semgrep" sub-command + Parameters + ---------- + parser: ArgumentParser + command line parser object + args: Namespace + Parsed arguments + """ + if (not args.purl and not args.input) or (args.purl and args.input): + print_stderr('Please specify an input file or purl to decorate (--purl or --input)') + parser.parse_args([args.subparser, args.subparsercmd, '-h']) + sys.exit(1) + if args.ca_cert and not os.path.exists(args.ca_cert): + print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.') + sys.exit(1) + pac_file = get_pac_file(args.pac) + comps = Components( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + grpc_url=args.apiurl, # Legacy param name; accepts the REST API base URL. TODO: rename to url + api_key=args.key, + ca_cert=args.ca_cert, + proxy=args.proxy, + pac=pac_file, + timeout=args.timeout, + req_headers=process_req_headers(args.header), + ) + if not comps.get_semgrep_details(args.input, args.purl, args.output): + sys.exit(1) + + +def comp_search(parser, args): + """ + Run the "component search" sub-command + Parameters + ---------- + parser: ArgumentParser + command line parser object + args: Namespace + Parsed arguments + """ + if (not args.input and not args.search and not args.vendor and not args.comp) or ( + args.input and (args.search or args.vendor or args.comp) + ): + print_stderr('Please specify an input file or search terms (--input or --search, or --vendor or --comp.)') + parser.parse_args([args.subparser, args.subparsercmd, '-h']) + sys.exit(1) + + if args.ca_cert and not os.path.exists(args.ca_cert): + print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.') + sys.exit(1) + pac_file = get_pac_file(args.pac) + comps = Components( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + grpc_url=args.apiurl, # Legacy param name; accepts the REST API base URL. TODO: rename to url + api_key=args.key, + ca_cert=args.ca_cert, + proxy=args.proxy, + pac=pac_file, + timeout=args.timeout, + req_headers=process_req_headers(args.header), + ) + if not comps.search_components( + args.output, + json_file=args.input, + search=args.search, + vendor=args.vendor, + comp=args.comp, + package=args.package, + limit=args.limit, + offset=args.offset, + ): + sys.exit(1) + + +def comp_versions(parser, args): + """ + Run the "component versions" sub-command + Parameters + ---------- + parser: ArgumentParser + command line parser object + args: Namespace + Parsed arguments + """ + if (not args.input and not args.purl) or (args.input and args.purl): + print_stderr('Please specify an input file or search terms (--input or --purl.)') + parser.parse_args([args.subparser, args.subparsercmd, '-h']) + sys.exit(1) + + if args.ca_cert and not os.path.exists(args.ca_cert): + print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.') + sys.exit(1) + pac_file = get_pac_file(args.pac) + comps = Components( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + grpc_url=args.apiurl, # Legacy param name; accepts the REST API base URL. TODO: rename to url + api_key=args.key, + ca_cert=args.ca_cert, + proxy=args.proxy, + pac=pac_file, + timeout=args.timeout, + req_headers=process_req_headers(args.header), + ) + if not comps.get_component_versions(args.output, json_file=args.input, purl=args.purl, limit=args.limit): + sys.exit(1) + + +def comp_provenance(parser, args): + """ + Run the "component provenance" sub-command + Parameters + ---------- + parser: ArgumentParser + command line parser object + args: Namespace + Parsed arguments + """ + if (not args.purl and not args.input) or (args.purl and args.input): + print_stderr('Please specify an input file or purl to decorate (--purl or --input)') + parser.parse_args([args.subparser, args.subparsercmd, '-h']) + sys.exit(1) + if args.ca_cert and not os.path.exists(args.ca_cert): + print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.') + sys.exit(1) + pac_file = get_pac_file(args.pac) + comps = Components( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + grpc_url=args.apiurl, # Legacy param name; accepts the REST API base URL. TODO: rename to url + api_key=args.key, + ca_cert=args.ca_cert, + proxy=args.proxy, + pac=pac_file, + timeout=args.timeout, + req_headers=process_req_headers(args.header), + ) + if not comps.get_provenance_details(args.input, args.purl, args.output, args.origin): + sys.exit(1) + + +def comp_licenses(parser, args): + """ + Run the "component licenses" sub-command + Parameters + ---------- + parser: ArgumentParser + command line parser object + args: Namespace + Parsed arguments + """ + if (not args.purl and not args.input) or (args.purl and args.input): + print_stderr('ERROR: Please specify an input file or purl to decorate (--purl or --input)') + parser.parse_args([args.subparser, args.subparsercmd, '-h']) + sys.exit(1) + if args.ca_cert and not os.path.exists(args.ca_cert): + print_stderr(f'ERROR: Certificate file does not exist: {args.ca_cert}.') + sys.exit(1) + pac_file = get_pac_file(args.pac) + comps = Components( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + grpc_url=args.apiurl, # Legacy param name; accepts the REST API base URL. TODO: rename to url + api_key=args.key, + ca_cert=args.ca_cert, + proxy=args.proxy, + pac=pac_file, + timeout=args.timeout, + req_headers=process_req_headers(args.header), + ) + if not comps.get_licenses(args.input, args.purl, args.output): + sys.exit(1) + + +def comp_status(parser, args): + """ + Run the "component status" sub-command + Parameters + ---------- + parser: ArgumentParser + command line parser object + args: Namespace + Parsed arguments + """ + if (not args.purl and not args.input) or (args.purl and args.input): + print_stderr('ERROR: Please specify an input file or purl to decorate (--purl or --input)') + parser.parse_args([args.subparser, args.subparsercmd, '-h']) + sys.exit(1) + if args.ca_cert and not os.path.exists(args.ca_cert): + print_stderr(f'ERROR: Certificate file does not exist: {args.ca_cert}.') + sys.exit(1) + pac_file = get_pac_file(args.pac) + comps = Components( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + grpc_url=args.apiurl, # Legacy param name; accepts the REST API base URL. TODO: rename to url + api_key=args.key, + ca_cert=args.ca_cert, + proxy=args.proxy, + pac=pac_file, + timeout=args.timeout, + req_headers=process_req_headers(args.header), + ignore_cert_errors=args.ignore_cert_errors, + ) + if not comps.get_status(args.input, args.purl, args.output): + sys.exit(1) + + +def results(parser, args): + """ + Run the "results" sub-command + Parameters + ---------- + parser: ArgumentParser + command line parser object + args: Namespace + Parsed arguments + """ + if not args.filepath: + print_stderr('ERROR: Please specify a file containing the results') + parser.parse_args([args.subparser, '-h']) + sys.exit(1) + + file_path = Path(args.filepath).resolve() + + if not file_path.is_file(): + print_stderr(f'The specified file {args.filepath} does not exist') + sys.exit(1) + + results = Results( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + filepath=file_path, + match_type=args.match_type, + status=args.status, + output_file=args.output, + output_format=args.format, + ) + + if args.has_pending: + results.get_pending_identifications().present() + if results.has_results(): + sys.exit(1) + else: + results.apply_filters().present() + + +def process_req_headers(headers_array: List[str]) -> dict: + """ + Process a list of header strings in the format "Name: Value" into a dictionary. + + Args: + headers_array (list): List of header strings from command line args + + Returns: + dict: Dictionary of header name-value pairs + """ + # Check if headers_array is empty + if not headers_array: + # Array is empty + return {} + + dict_headers = {} + for header_str in headers_array: + # Split each "Name: Value" header + parts = header_str.split(':', 1) + if len(parts) == HEADER_PARTS_COUNT: + name = parts[0].strip() + value = parts[1].strip() + dict_headers[name] = value + return dict_headers + + +def folder_hashing_scan(parser, args): + """Run the "folder-scan" sub-command + + Args: + parser (ArgumentParser): command line parser object + args (Namespace): Parsed arguments + """ + try: + if not args.scan_dir: + print_stderr('ERROR: Please specify a directory to scan') + parser.parse_args([args.subparser, '-h']) + sys.exit(1) + + if not os.path.exists(args.scan_dir) or not os.path.isdir(args.scan_dir): + print_stderr(f'ERROR: The specified directory {args.scan_dir} does not exist') + sys.exit(1) + + scanner_config = create_scanner_config_from_args(args) + scanoss_settings = get_scanoss_settings_from_args(args) + grpc_config = create_grpc_config_from_args(args) + + client = ScanossGrpc(**asdict(grpc_config)) + + scanner = ScannerHFH( + scan_dir=args.scan_dir, + config=scanner_config, + client=client, + scanoss_settings=scanoss_settings, + rank_threshold=args.rank_threshold, + depth=args.depth, + recursive_threshold=args.recursive_threshold, + min_accepted_score=args.min_accepted_score, + ) + + if scanner.scan(): + scanner.present(output_file=args.output, output_format=args.format) + except ScanossGrpcError as e: + print_stderr(f'ERROR: {e}') + sys.exit(1) + + +def folder_hash(parser, args): + """Run the "folder-hash" sub-command + + Args: + parser (ArgumentParser): command line parser object + args (Namespace): Parsed arguments + """ + try: + if not args.scan_dir: + print_stderr('ERROR: Please specify a directory to scan') + parser.parse_args([args.subparser, '-h']) + sys.exit(1) + + if not os.path.exists(args.scan_dir) or not os.path.isdir(args.scan_dir): + print_stderr(f'ERROR: The specified directory {args.scan_dir} does not exist') + sys.exit(1) + + folder_hasher_config = create_folder_hasher_config_from_args(args) + scanoss_settings = get_scanoss_settings_from_args(args) + + folder_hasher = FolderHasher( + scan_dir=args.scan_dir, + config=folder_hasher_config, + scanoss_settings=scanoss_settings, + depth=args.depth, + ) + + folder_hasher.hash_directory(args.scan_dir) + folder_hasher.present(output_file=args.output, output_format=args.format) + except Exception as e: + print_stderr(f'ERROR: {e}') + sys.exit(1) + + +def container_scan(parser, args, only_interim_results: bool = False): + """ + Run the "container-scan" sub-command + Parameters + ---------- + parser: ArgumentParser + command line parser object + args: Namespace + Parsed arguments + """ + if not args.scan_loc: + print_stderr( + 'Please specify a container image, Docker tar, OCI tar, OCI directory, SIF Container, or directory to scan' + ) + parser.parse_args([args.subparser, '-h']) + sys.exit(1) + + try: + config = create_container_scanner_config_from_args(args) + config.only_interim_results = only_interim_results + container_scanner = ContainerScanner( + config=config, + what_to_scan=args.scan_loc, + ) + + container_scanner.scan() + if only_interim_results: + container_scanner.present(output_file=config.output, output_format='raw') + else: + container_scanner.decorate_scan_results_with_dependencies() + container_scanner.present(output_file=config.output, output_format=config.format) + except Exception as e: + print_stderr(f'ERROR: {e}') + sys.exit(1) + + +def get_scanoss_settings_from_args(args): + scanoss_settings = None + if not args.skip_settings_file: + scanoss_settings = ScanossSettings(debug=args.debug, trace=args.trace, quiet=args.quiet) + try: + scanoss_settings.load_json_file(args.settings, args.scan_dir).set_file_type('new').set_scan_type('identify') + except ScanossSettingsError as e: + print_stderr(f'Error: {e}') + sys.exit(1) + return scanoss_settings + + +def initialise_empty_file(filename: str): + """ + Initialises an empty file with the specified name. If the file already exists, + it truncates its content. Ensures proper error handling in case of failure. + + Args: + filename (str): The name of the file to be initialised. + + Raises: + SystemExit: If the file cannot be created or written due to an exception, + the function prints an error message and exits the program. + + Note: + This function writes an empty file and handles exceptions to ensure the + program does not continue execution in case of an error. + """ + if filename: + try: + open(filename, 'w').close() + except Exception as e: + print_stderr(f'Error: Unable to create output file {filename}: {e}') + sys.exit(1) + + +def delta_copy(parser, args): + """ + Handle delta copy command. + + Copies files listed in an input file to a target directory while preserving + their directory structure. Creates a unique delta directory if none is specified. + + Parameters + ---------- + parser : ArgumentParser + Command line parser object for help display + args : Namespace + Parsed command line arguments containing: + - input: Path to file containing list of files to copy + - folder: Optional target directory path + - output: Optional output file path + """ + # Validate required input file parameter + if args.input is None: + print_stderr('ERROR: Input file is required for copying') + parser.parse_args([args.subparser, args.subparsercmd, '-h']) + sys.exit(1) + # Initialise output file if specified if args.output: - scan_output = args.output - open(scan_output, 'w').close() + initialise_empty_file(args.output) + try: + # Create and configure delta copy command + delta = Delta( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + filepath=args.input, + folder=args.folder, + output=args.output, + root_dir=args.root, + ) + # Execute copy and exit with appropriate status code + status, _ = delta.copy() + sys.exit(status) + except Exception as e: + print_stderr(e) + if args.debug: + traceback.print_exc() + sys.exit(1) - sc_deps = ScancodeDeps(debug=args.debug, quiet=args.quiet, trace=args.trace, sc_command=args.sc_command, - timeout=args.sc_timeout - ) - if not sc_deps.get_dependencies(what_to_scan=args.scan_dir, result_output=scan_output): - exit(1) def main(): """ @@ -354,5 +2970,5 @@ def main(): setup_args() -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/src/scanoss/components.py b/src/scanoss/components.py new file mode 100644 index 00000000..34cf2212 --- /dev/null +++ b/src/scanoss/components.py @@ -0,0 +1,430 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2023, SCANOSS + + 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. +""" + +import json +import os +import sys +from typing import List, Optional, TextIO + +from pypac.parser import PACFile + +from scanoss.cyclonedx import CycloneDx +from scanoss.utils.file import validate_json_file + +from .scanner import Scanner +from .scanossbase import ScanossBase +from .scanossgrpc import ScanossGrpc + + +class Components(ScanossBase): + """ + Class for Component functionality + """ + + def __init__( # noqa: PLR0913, PLR0915 + self, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + grpc_url: str = None, + api_key: str = None, + timeout: int = 600, + proxy: str = None, + grpc_proxy: str = None, + ca_cert: str = None, + pac: PACFile = None, + req_headers: dict = None, + ignore_cert_errors: bool = False, + use_grpc: bool = False, + ): + """ + Handle all component style requests + + :param debug: Debug + :param trace: Trace + :param quiet: Quiet + :param grpc_url: gRPC URL + :param api_key: API Key + :param timeout: Timeout for requests (default 600) + :param proxy: Proxy to use (optional) + :param grpc_proxy: Specific gRPC proxy (optional) + :param ca_cert: TLS client certificate (optional) + :param pac: Proxy Auto-Config file (optional) + :param req_headers: Additional headers to send with requests (optional) + :param ignore_cert_errors: Ignore TLS certificate errors (optional) + :param use_grpc: Use gRPC instead of HTTP (optional) + """ + super().__init__(debug, trace, quiet) + ver_details = Scanner.version_details() + self.use_grpc = use_grpc + self.grpc_api = ScanossGrpc( + url=grpc_url, + debug=debug, + quiet=quiet, + trace=trace, + api_key=api_key, + ver_details=ver_details, + ca_cert=ca_cert, + proxy=proxy, + pac=pac, + grpc_proxy=grpc_proxy, + timeout=timeout, + req_headers=req_headers, + ignore_cert_errors=ignore_cert_errors, + ) + self.cdx = CycloneDx(debug=self.debug) + + def load_comps(self, json_file: Optional[str] = None, purls: Optional[List[str]] = None) -> Optional[dict]: + """ + Load the specified components and return a dictionary + + :param json_file: JSON Components file (optional) + :param purls: list pf PURLs (optional) + :return: Components Request dictionary or None + """ + return self.load_purls(json_file, purls, 'components') + + def load_purls( + self, json_file: Optional[str] = None, purls: Optional[List[str]] = None, field: str = 'purls' + ) -> Optional[dict]: + """ + Load the specified purls and return a dictionary + + :param json_file: JSON PURL file (optional) + :param purls: list of PURLs (optional) + :param field: Name of the dictionary field to store the purls in (default: 'purls') + :return: PURL Request dictionary or None + """ + if json_file: + result = validate_json_file(json_file) + if not result.is_valid: + self.print_stderr(f'ERROR: Problem parsing input JSON: {result.error}') + return None + + if self.cdx.is_cyclonedx_json(json.dumps(result.data)): + purl_request = self.cdx.get_purls_request_from_cdx(result.data, field) + else: + purl_request = result.data + elif purls: + if not all(isinstance(purl, str) for purl in purls): + self.print_stderr('ERROR: PURLs must be a list of strings.') + return None + parsed_purls = [] + for p in purls: + parsed_purls.append({'purl': p}) + purl_request = {field: parsed_purls} + else: + self.print_stderr('ERROR: No purls specified to process.') + return None + purl_count = len(purl_request.get(field, [])) + self.print_debug(f'Parsed {field} ({purl_count}): {purl_request}') + if purl_count == 0: + self.print_stderr(f'ERROR: No {field} parsed from request.') + return None + return purl_request + + def load_json(self, json_file: str = None) -> dict: + """ + Load the specified json and return a dictionary + + :param json_file: JSON PURL file + :return: PURL Request dictionary + """ + if json_file: + if not os.path.isfile(json_file) or not os.access(json_file, os.R_OK): + self.print_stderr(f'ERROR: JSON file does not exist, is not a file, or is not readable: {json_file}') + return None + with open(json_file, 'r') as f: + try: + return json.loads(f.read()) + except Exception as e: + self.print_stderr(f'ERROR: Problem parsing input JSON: {e}') + return None + + def _open_file_or_sdtout(self, filename): + """ + Open the given filename if requested, otherwise return STDOUT + + :param filename: filename to open or None to return STDOUT + :return: file descriptor or None + """ + file = sys.stdout + if filename: + try: + file = open(filename, 'w') + except OSError as e: + self.print_stderr(f'ERROR: Failed to open output file {filename}: {e}') + return None + return file + + def _close_file(self, filename: str = None, file: TextIO = None) -> None: + """ + Close the file descriptor if its defined + + :param filename: filename + :param file: file IO object + :return: None + """ + if filename and file: + self.print_trace(f'Closing file: {filename}') + file.close() + + def get_vulnerabilities(self, json_file: str = None, purls: [] = None, output_file: str = None) -> bool: + """ + Retrieve any vulnerabilities related to the given PURLs + + :param json_file: PURL JSON request file (optional) + :param purls: PURL request array (optional) + :param output_file: output filename (optional). Default: STDOUT + :return: True on success, False otherwise + """ + success = False + purls_request = self.load_comps(json_file, purls) + if purls_request is None or len(purls_request) == 0: + return False + file = self._open_file_or_sdtout(output_file) + if file is None: + return False + self.print_msg('Sending PURLs to Vulnerability API for decoration...') + response = self.grpc_api.get_vulnerabilities_json(purls_request, use_grpc=self.use_grpc) + if response: + print(json.dumps(response, indent=2, sort_keys=True), file=file) + success = True + if output_file: + self.print_msg(f'Results written to: {output_file}') + self._close_file(output_file, file) + return success + + def get_semgrep_details(self, json_file: str = None, purls: [] = None, output_file: str = None) -> bool: + """ + Retrieve the semgrep details for the supplied PURLs + + :param json_file: PURL JSON request file (optional) + :param purls: PURL request array (optional) + :param output_file: output filename (optional). Default: STDOUT + :return: True on success, False otherwise + """ + success = False + purls_request = self.load_comps(json_file, purls) + if purls_request is None or len(purls_request) == 0: + return False + file = self._open_file_or_sdtout(output_file) + if file is None: + return False + self.print_msg('Sending PURLs to Semgrep API for decoration...') + response = self.grpc_api.get_semgrep_json(purls_request, use_grpc=self.use_grpc) + if response: + print(json.dumps(response, indent=2, sort_keys=True), file=file) + success = True + if output_file: + self.print_msg(f'Results written to: {output_file}') + self._close_file(output_file, file) + return success + + def search_components( # noqa: PLR0913, PLR0915 + self, + output_file: str = None, + json_file: str = None, + search: str = None, + vendor: str = None, + comp: str = None, + package: str = None, + limit: int = None, + offset: int = None, + ) -> bool: + """ + Search for a component based on the given search criteria + + :param output_file: output filename (optional). Default: STDOUT + :param json_file: Search JSON request file (optional) + :param search: Search for (vendor/component/purl) for a component (overrides vendor/component) + :param vendor: Vendor to search for + :param comp: Component to search for + :param package: Package (purl type) to search for. i.e. github/maven/maven/npn/all - default github + :param limit: Number of matches to return + :param offset: Offset to submit to return next (limit) of component matches + :return: True on success, False otherwise + """ + success = False + request: dict + if json_file: # Parse the json file to extract the search details + request = self.load_json(json_file) + if request is None: + return False + else: # Construct a query dictionary from parameters + request = {'search': search, 'vendor': vendor, 'component': comp, 'package': package} + if limit is not None and limit > 0: + request['limit'] = limit + if offset is not None and offset > 0: + request['offset'] = offset + + file = self._open_file_or_sdtout(output_file) + if file is None: + return False + self.print_msg('Sending search data to Components API...') + response = self.grpc_api.search_components_json(request, use_grpc=self.use_grpc) + if response: + print(json.dumps(response, indent=2, sort_keys=True), file=file) + success = True + if output_file: + self.print_msg(f'Results written to: {output_file}') + self._close_file(output_file, file) + return success + + def get_component_versions( + self, output_file: str = None, json_file: str = None, purl: str = None, limit: int = None + ) -> bool: + """ + Search for a component versions based on the given search criteria + + :param output_file: output filename (optional). Default: STDOUT + :param json_file: Search JSON request file (optional) + :param purl: PURL to retrieve versions for + :param limit: Number of version to return + :return: True on success, False otherwise + """ + success = False + request: dict + if json_file: # Parse the json file to extract the search details + request = self.load_json(json_file) + if request is None: + return False + else: # Construct a query dictionary from parameters + request = {'purl': purl} + if limit is not None and limit > 0: + request['limit'] = limit + + file = self._open_file_or_sdtout(output_file) + if file is None: + return False + self.print_msg('Sending PURLs to Component Versions API...') + response = self.grpc_api.get_component_versions_json(request, use_grpc=self.use_grpc) + if response: + print(json.dumps(response, indent=2, sort_keys=True), file=file) + success = True + if output_file: + self.print_msg(f'Results written to: {output_file}') + self._close_file(output_file, file) + return success + + def get_provenance_details( + self, json_file: str = None, purls: [] = None, output_file: str = None, origin: bool = False + ) -> bool: + """ + Retrieve the provenance details for the supplied PURLs + + Args: + json_file (str, optional): Input JSON file. Defaults to None. + purls (None, optional): PURLs to retrieve provenance details for. Defaults to None. + output_file (str, optional): Output file. Defaults to None. + origin (bool, optional): Retrieve origin details. Defaults to False. + + Returns: + bool: True on success, False otherwise + """ + success = False + purls_request = self.load_comps(json_file, purls) + if purls_request is None or len(purls_request) == 0: + return False + file = self._open_file_or_sdtout(output_file) + if file is None: + return False + if origin: + self.print_msg('Sending PURLs to Geo Provenance Origin API for decoration...') + response = self.grpc_api.get_provenance_origin(purls_request, use_grpc=self.use_grpc) + else: + self.print_msg('Sending PURLs to Geo Provenance Declared API for decoration...') + response = self.grpc_api.get_provenance_json(purls_request, use_grpc=self.use_grpc) + if response: + print(json.dumps(response, indent=2, sort_keys=True), file=file) + success = True + if output_file: + self.print_msg(f'Results written to: {output_file}') + self._close_file(output_file, file) + return success + + def get_licenses(self, json_file: str = None, purls: [] = None, output_file: str = None) -> bool: + """ + Retrieve the license details for the supplied PURLs + + Args: + json_file (str, optional): Input JSON file. Defaults to None. + purls (None, optional): PURLs to retrieve license details for. Defaults to None. + output_file (str, optional): Output file. Defaults to None. + + Returns: + bool: True on success, False otherwise + """ + success = False + + purls_request = self.load_purls(json_file, purls) + if not purls_request: + return False + file = self._open_file_or_sdtout(output_file) + if file is None: + return False + + # We'll use the new ComponentBatchRequest instead of deprecated PurlRequest for the license api + component_batch_request = {'components': purls_request.get('purls')} + response = self.grpc_api.get_licenses(component_batch_request, use_grpc=self.use_grpc) + if response: + print(json.dumps(response, indent=2, sort_keys=True), file=file) + success = True + if output_file: + self.print_msg(f'Results written to: {output_file}') + self._close_file(output_file, file) + return success + + def get_status(self, json_file: str = None, purls: [] = None, output_file: str = None) -> bool: + """ + Retrieve the development status details for the supplied PURLs + + Args: + json_file (str, optional): Input JSON file. Defaults to None. + purls (None, optional): PURLs to retrieve status details for. Defaults to None. + output_file (str, optional): Output file. Defaults to None. + + Returns: + bool: True on success, False otherwise + """ + success = False + + purls_request = self.load_purls(json_file, purls) + if not purls_request: + return False + file = self._open_file_or_sdtout(output_file) + if file is None: + return False + + # Use ComponentBatchRequest format for the status api + component_batch_request = {'components': purls_request.get('purls')} + self.print_msg('Sending PURLs to Component Status API...') + response = self.grpc_api.get_component_status(component_batch_request, use_grpc=self.use_grpc) + if response: + print(json.dumps(response, indent=2, sort_keys=True), file=file) + success = True + if output_file: + self.print_msg(f'Results written to: {output_file}') + self._close_file(output_file, file) + return success diff --git a/src/scanoss/constants.py b/src/scanoss/constants.py new file mode 100644 index 00000000..68216595 --- /dev/null +++ b/src/scanoss/constants.py @@ -0,0 +1,22 @@ +DEFAULT_POST_SIZE = 32 +DEFAULT_TIMEOUT = 180 +DEFAULT_RETRY = 5 +MIN_TIMEOUT = 5 + +PYTHON_MAJOR_VERSION = 3 + +DEFAULT_SC_TIMEOUT = 600 +DEFAULT_NB_THREADS = 5 + +DEFAULT_URL = 'https://api.osskb.org' # default free service URL +DEFAULT_URL2 = 'https://api.scanoss.com' # default premium service URL + +DEFAULT_API_TIMEOUT = 600 + +DEFAULT_HFH_RANK_THRESHOLD = 5 +DEFAULT_HFH_DEPTH = 1 +DEFAULT_HFH_RECURSIVE_THRESHOLD = 0.8 +DEFAULT_HFH_MIN_ACCEPTED_SCORE = 0.15 + +VALID_LICENSE_SOURCES = ['component_declared', 'license_file', 'file_header', 'file_spdx_tag', 'scancode'] +DEFAULT_COPYLEFT_LICENSE_SOURCES = ['component_declared', 'license_file'] diff --git a/src/scanoss/cryptography.py b/src/scanoss/cryptography.py new file mode 100644 index 00000000..5ab91fea --- /dev/null +++ b/src/scanoss/cryptography.py @@ -0,0 +1,308 @@ +import json +from dataclasses import dataclass +from typing import Dict, List, Optional + +from scanoss.cyclonedx import CycloneDx +from scanoss.scanossbase import ScanossBase +from scanoss.scanossgrpc import ScanossGrpc +from scanoss.utils.abstract_presenter import AbstractPresenter +from scanoss.utils.file import validate_json_file + + +class ScanossCryptographyError(Exception): + pass + + +MIN_SPLIT_PARTS = 2 + + +@dataclass +class CryptographyConfig: + purl: List[str] + debug: bool = False + header: Optional[str] = None + input_file: Optional[str] = None + output_file: Optional[str] = None + quiet: bool = False + trace: bool = False + use_grpc: bool = False + with_range: bool = False + + def _process_input_file(self) -> dict: + """ + Process and validate the input file, returning the validated purl_request. + + Returns: + dict: The validated purl_request dictionary + + Raises: + ScanossCryptographyError: If the input file is invalid + """ + result = validate_json_file(self.input_file) + if not result.is_valid: + raise ScanossCryptographyError( + f'There was a problem with the purl input file. {result.error}' + ) + + cdx = CycloneDx(debug=self.debug) + if cdx.is_cyclonedx_json(json.dumps(result.data)): + purl_request = cdx.get_purls_request_from_cdx(result.data) + else: + purl_request = result.data + + if ( + not isinstance(purl_request, dict) + or 'purls' not in purl_request + or not isinstance(purl_request['purls'], list) + or not all(isinstance(p, dict) and 'purl' in p for p in purl_request['purls']) + ): + raise ScanossCryptographyError('The supplied input file is not in the correct PurlRequest format.') + + return purl_request + + def __post_init__(self): + """ + Validate that the configuration is valid. + """ + if self.purl: + if self.with_range: + for purl in self.purl: + parts = purl.split('@') + if not (len(parts) >= MIN_SPLIT_PARTS and parts[1]): + raise ScanossCryptographyError( + f'Invalid PURL format: "{purl}".It must include a version (e.g., pkg:type/name@version)' + ) + if self.input_file: + purl_request = self._process_input_file() + purls = purl_request['purls'] + purls_with_requirement = [] + if self.with_range and any('requirement' not in p for p in purls): + raise ScanossCryptographyError( + f'One or more PURLs in "{self.input_file}" are missing the "requirement" field.' + ) + + for purl in purls: + if 'requirement' in purl: + purls_with_requirement.append(f'{purl["purl"]}@{purl["requirement"]}') + else: + purls_with_requirement.append(purl['purl']) + self.purl = purls_with_requirement + + +def create_cryptography_config_from_args(args) -> CryptographyConfig: + return CryptographyConfig( + debug=getattr(args, 'debug', False), + header=getattr(args, 'header', None), + input_file=getattr(args, 'input', None), + output_file=getattr(args, 'output', None), + purl=getattr(args, 'purl', []), + quiet=getattr(args, 'quiet', False), + trace=getattr(args, 'trace', False), + use_grpc=getattr(args, 'grpc', False), + with_range=getattr(args, 'with_range', False), + ) + + +class Cryptography: + """ + Cryptography Class + + This class is used to decorate purls with cryptography information. + """ + + def __init__( + self, + config: CryptographyConfig, + client: ScanossGrpc, + ): + """ + Initialize the Cryptography. + + Args: + config (CryptographyConfig): Configuration parameters for the cryptography. + client (ScanossGrpc): gRPC client for communicating with the scanning service. + """ + self.base = ScanossBase( + debug=config.debug, + trace=config.trace, + quiet=config.quiet, + ) + self.presenter = CryptographyPresenter( + self, + debug=config.debug, + trace=config.trace, + quiet=config.quiet, + ) + + self.client = client + self.config = config + self.components_request = self._build_components_request() + self.results = None + + def get_algorithms(self) -> Optional[Dict]: + """ + Get the cryptographic algorithms for the provided purl or input file. + + Returns: + Optional[Dict]: The folder hash response from the gRPC client, or None if an error occurs. + """ + + if not self.components_request or not self.components_request.get('components'): + raise ScanossCryptographyError('No PURLs supplied. Provide --purl or --input.') + components_str = ', '.join(p['purl'] for p in self.components_request['components']) + self.base.print_stderr(f'Getting cryptographic algorithms for {components_str}') + if self.config.with_range: + response = self.client.get_crypto_algorithms_in_range_for_purl( + self.components_request, self.config.use_grpc + ) + else: + response = self.client.get_crypto_algorithms_for_purl(self.components_request, self.config.use_grpc) + if response: + self.results = response + + return self.results + + def get_encryption_hints(self) -> Optional[Dict]: + """ + Get the encryption hints for the provided purl or input file. + + Returns: + Optional[Dict]: The encryption hints response from the gRPC client, or None if an error occurs. + """ + + if not self.components_request or not self.components_request.get('components'): + raise ScanossCryptographyError('No PURLs supplied. Provide --purl or --input.') + self.base.print_stderr( + f'Getting encryption hints ' + f'{"in range" if self.config.with_range else ""} ' + f'for {", ".join([p["purl"] for p in self.components_request["components"]])}' + ) + if self.config.with_range: + response = self.client.get_encryption_hints_in_range_for_purl(self.components_request, self.config.use_grpc) + else: + response = self.client.get_encryption_hints_for_purl(self.components_request, self.config.use_grpc) + if response: + self.results = response + + return self.results + + def get_versions_in_range(self) -> Optional[Dict]: + """ + Given a list of PURLS and version ranges, get a list of versions that do/do not contain cryptographic algorithms + + Returns: + Optional[Dict]: The versions in range response from the gRPC client, or None if an error occurs. + """ + + if not self.components_request or not self.components_request.get('components'): + raise ScanossCryptographyError('No PURLs supplied. Provide --purl or --input.') + + components_str = ', '.join(p['purl'] for p in self.components_request['components']) + self.base.print_stderr(f'Getting versions in range for {components_str}') + + response = self.client.get_versions_in_range_for_purl(self.components_request, self.config.use_grpc) + if response: + self.results = response + + return self.results + + def _build_components_request( + self, + ) -> Optional[dict]: + """ + Load the specified purls from a JSON file or a list of PURLs and return a dictionary + + Returns: + Optional[dict]: The dictionary containing the PURLs + """ + return { + 'components': [ + { + 'purl': self._remove_version_from_purl(purl), + 'requirement': self._extract_version_from_purl(purl), + } + for purl in self.config.purl + ] + } + + def _remove_version_from_purl(self, purl: str) -> str: + """ + Remove version from purl + + Args: + purl (str): The purl string to remove the version from + + Returns: + str: The purl string without the version + """ + if '@' not in purl: + return purl + return purl.split('@')[0] + + def _extract_version_from_purl(self, purl: str) -> str: + """ + Extract version from purl + + Args: + purl (str): The purl string to extract the version from + + Returns: + str: The extracted version + + Raises: + ScanossCryptographyError: If the purl is not in the correct format + """ + try: + return purl.split('@')[-1] + except IndexError: + raise ScanossCryptographyError(f'Invalid purl format: {purl}') + + def present( + self, + output_format: Optional[str] = None, + output_file: Optional[str] = None, + ): + """Present the results in the selected format""" + self.presenter.present(output_format=output_format, output_file=output_file) + + +class CryptographyPresenter(AbstractPresenter): + """ + Cryptography presenter class + Handles the presentation of the cryptography results + """ + + def __init__(self, cryptography: Cryptography, **kwargs): + super().__init__(**kwargs) + self.cryptography = cryptography + + def _format_json_output(self) -> str: + """ + Format the scan output data into a JSON object + + Returns: + str: The formatted JSON string + """ + return json.dumps(self.cryptography.results, indent=2) + + def _format_plain_output(self) -> str: + """ + Format the scan output data into a plain text string + """ + return ( + json.dumps(self.cryptography.results, indent=2) + if isinstance(self.cryptography.results, dict) + else str(self.cryptography.results) + ) + + def _format_cyclonedx_output(self) -> str: + raise NotImplementedError('CycloneDX output is not implemented') + + def _format_spdxlite_output(self) -> str: + raise NotImplementedError('SPDXlite output is not implemented') + + def _format_csv_output(self) -> str: + raise NotImplementedError('CSV output is not implemented') + + def _format_raw_output(self) -> str: + raise NotImplementedError('Raw output is not implemented') diff --git a/src/scanoss/csvoutput.py b/src/scanoss/csvoutput.py new file mode 100644 index 00000000..5a7b1974 --- /dev/null +++ b/src/scanoss/csvoutput.py @@ -0,0 +1,244 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2022, SCANOSS + + 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. +""" +import csv +import json +import os.path +import sys + +from .scanossbase import ScanossBase + + +class CsvOutput(ScanossBase): + """ + CsvOutput management class + Handle all interaction with CSV formatting + """ + + def __init__(self, debug: bool = False, output_file: str = None): + """ + Initialise the CsvOutput class + """ + super().__init__(debug) + self.output_file = output_file + self.debug = debug + + # TODO Refactor (fails linter) + def parse(self, data: json): #noqa PLR0912, PLR0915 + """ + Parse the given input (raw/plain) JSON string and return CSV summary + :param data: json - JSON object + :return: CSV dictionary + """ + if data is None: + self.print_stderr('ERROR: No JSON data provided to parse.') + return None + if len(data) == 0: + self.print_msg('Warning: Empty scan results provided. Returning empty CSV list.') + return [] + self.print_debug('Processing raw results into CSV format...') + csv_dict = [] + row_id = 1 + for f in data: + file_details = data.get(f) + # print(f'File: {f}: {file_details}') + for d in file_details: + id_details = d.get('id') + if not id_details or id_details == 'none': + continue + matched = d.get('matched', '') + lines = d.get('lines', '').replace(',', ';') # swap comma with semicolon to help basic parsers + oss_lines = d.get('oss_lines', '').replace(',', ';') + detected = {} + if id_details == 'dependency': + dependencies = d.get('dependencies') + if not dependencies: + self.print_stderr(f'Warning: No Dependencies found for {f}: {file_details}') + continue + for deps in dependencies: + detected = {} + purl = deps.get('purl') + if not purl: + self.print_stderr(f'Warning: No PURL found for {f}: {deps}') + continue + detected['purls'] = purl + for field in ['component', 'version', 'latest', 'url']: + detected[field] = deps.get(field, '') + licenses = deps.get('licenses') + dc = [] + if licenses: + for lic in licenses: + name = lic.get('name') + if name and name not in dc: # Only save the license name once + dc.append(name) + if not dc or len(dc) == 0: + detected['licenses'] = '' + else: + detected['licenses'] = ';'.join(dc) + # inventory_id,path,usage,detected_component,detected_license, + # detected_version,detected_latest,purl + csv_dict.append( + { + 'inventory_id': row_id, + 'path': f, + 'detected_usage': id_details, + 'detected_component': detected.get('component'), + 'detected_license': detected.get('licenses'), + 'detected_version': detected.get('version'), + 'detected_latest': detected.get('latest'), + 'detected_purls': detected.get('purls'), + 'detected_url': detected.get('url'), + 'detected_path': detected.get('file', ''), + 'detected_match': matched, + 'detected_lines': lines, + 'detected_oss_lines': oss_lines, + } + ) + row_id = row_id + 1 + else: + purls = d.get('purl') + if not purls: + self.print_stderr(f'Warning: Purl block missing for {f}: {file_details}') + continue + pa = [] + for p in purls: + self.print_debug(f'Purl: {p}') + pa.append(p) + if not pa or len(pa) == 0: + self.print_stderr(f'Warning: No PURL found for {f}: {file_details}') + continue + detected['purls'] = ';'.join(pa) + for field in ['component', 'version', 'latest', 'url', 'file']: + detected[field] = d.get(field, '') + licenses = d.get('licenses') + dc = [] + if licenses: + for lic in licenses: + name = lic.get('name') + if name and name not in dc: # Only save the license name once + dc.append(lic.get('name')) + if not dc or len(dc) == 0: + detected['licenses'] = '' + else: + detected['licenses'] = ';'.join(dc) + # inventory_id,path,usage,detected_component,detected_license,detected_version,detected_latest,purl + csv_dict.append( + { + 'inventory_id': row_id, + 'path': f, + 'detected_usage': id_details, + 'detected_component': detected.get('component'), + 'detected_license': detected.get('licenses'), + 'detected_version': detected.get('version'), + 'detected_latest': detected.get('latest'), + 'detected_purls': detected.get('purls'), + 'detected_url': detected.get('url'), + 'detected_path': detected.get('file', ''), + 'detected_match': matched, + 'detected_lines': lines, + 'detected_oss_lines': oss_lines, + } + ) + row_id = row_id + 1 + return csv_dict + + def produce_from_file(self, json_file: str, output_file: str = None) -> bool: + """ + Parse plain/raw input JSON file and produce CSV output + :param json_file: + :param output_file: + :return: True if successful, False otherwise + """ + if not json_file: + self.print_stderr('ERROR: No JSON file provided to parse.') + return False + if not os.path.isfile(json_file): + self.print_stderr(f'ERROR: JSON file does not exist or is not a file: {json_file}') + return False + with open(json_file, 'r') as f: + success = self.produce_from_str(f.read(), output_file) + return success + + def produce_from_json(self, data: json, output_file: str = None) -> bool: + """ + Produce the CSV output from the input data + :param data: JSON object + :param output_file: Output file (optional) + :return: True if successful, False otherwise + """ + csv_data = self.parse(data) + if csv_data is None: + self.print_stderr('ERROR: No CSV data returned for the JSON string provided.') + return False + if len(csv_data) == 0: + self.print_msg('Warning: Empty scan results - generating CSV with headers only.') + # Header row/column details + fields = [ + 'inventory_id', + 'path', + 'detected_usage', + 'detected_component', + 'detected_license', + 'detected_version', + 'detected_latest', + 'detected_purls', + 'detected_url', + 'detected_match', + 'detected_lines', + 'detected_oss_lines', + 'detected_path', + ] + file = sys.stdout + if not output_file and self.output_file: + output_file = self.output_file + if output_file: + file = open(output_file, 'w') + writer = csv.DictWriter(file, fieldnames=fields) + writer.writeheader() # writing headers (field names) + writer.writerows(csv_data) # writing data rows + if output_file: + file.close() + + return True + + def produce_from_str(self, json_str: str, output_file: str = None) -> bool: + """ + Produce CSV output from input JSON string + :param json_str: input JSON string + :param output_file: Output file (optional) + :return: True if successful, False otherwise + """ + if not json_str: + self.print_stderr('ERROR: No JSON string provided to parse.') + return False + try: + data = json.loads(json_str) + except Exception as e: + self.print_stderr(f'ERROR: Problem parsing input JSON: {e}') + return False + return self.produce_from_json(data, output_file) + + +# +# End of CsvOutput Class +# diff --git a/src/scanoss/cyclonedx.py b/src/scanoss/cyclonedx.py index 81be87c0..ddf10912 100644 --- a/src/scanoss/cyclonedx.py +++ b/src/scanoss/cyclonedx.py @@ -1,33 +1,39 @@ """ - SPDX-License-Identifier: MIT - - Copyright (c) 2021, SCANOSS - - 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. +SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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. """ + +import datetime import json import os.path import sys -import hashlib -import time +import uuid + +from cyclonedx.schema import SchemaVersion +from cyclonedx.validation.json import JsonValidator +from . import __version__ from .scanossbase import ScanossBase +from .spdxlite import SpdxLite class CycloneDx(ScanossBase): @@ -43,33 +49,38 @@ def __init__(self, debug: bool = False, output_file: str = None): super().__init__(debug) self.output_file = output_file self.debug = debug + self._spdx = SpdxLite(debug=debug) - def parse(self, data: json): + def parse(self, data: dict): # noqa: PLR0912, PLR0915 """ Parse the given input (raw/plain) JSON string and return CycloneDX summary - :param data: json - JSON object - :return: CycloneDX dictionary + :param data: dict - JSON object + :return: CycloneDX dictionary, and vulnerability dictionary """ - if not data: + if data is None: self.print_stderr('ERROR: No JSON data provided to parse.') - return None - self.print_debug(f'Processing raw results into CycloneDX format...') + return None, None + if len(data) == 0: + self.print_msg('Warning: Empty scan results provided. Returning empty component dictionary.') + return {}, {} + self.print_debug('Processing raw results into CycloneDX format...') cdx = {} + vdx = {} for f in data: file_details = data.get(f) # print(f'File: {f}: {file_details}') for d in file_details: - id_details = d.get("id") + id_details = d.get('id') if not id_details or id_details == 'none': continue purl = None if id_details == 'dependency': - dependencies = d.get("dependencies") + dependencies = d.get('dependencies') if not dependencies: self.print_stderr(f'Warning: No Dependencies found for {f}: {file_details}') continue for deps in dependencies: - purl = deps.get("purl") + purl = deps.get('purl') if not purl: self.print_stderr(f'Warning: No PURL found for {f}: {deps}') continue @@ -81,12 +92,13 @@ def parse(self, data: json): fd[field] = deps.get(field, '') licenses = deps.get('licenses') fdl = [] - dc = [] - for lic in licenses: - name = lic.get("name") - if name not in dc: # Only save the license name once - fdl.append({'id': name}) - dc.append(name) + if licenses: + dc = [] + for lic in licenses: + name = lic.get('name') + if name not in dc: # Only save the license name once + fdl.append({'id': name}) + dc.append(name) fd['licenses'] = fdl cdx[purl] = fd else: @@ -101,19 +113,55 @@ def parse(self, data: json): if not purl: self.print_stderr(f'Warning: No PURL found for {f}: {file_details}') continue + fd = {} + vulnerabilities = d.get('vulnerabilities') + if vulnerabilities: + for vuln in vulnerabilities: + vuln_id = vuln.get('ID') + if vuln_id == '': + vuln_id = vuln.get('id') + if not vuln_id or vuln_id == '': # Skip empty ids + continue + vuln_cve = vuln.get('CVE', '') + if vuln_cve == '': + vuln_cve = vuln.get('cve', '') + if vuln_id.upper().startswith('CPE:'): + fd['cpe'] = vuln_id # Save the component CPE if we have one + if vuln_cve != '': + vuln_id = vuln_cve + vd = vdx.get(vuln_id) # Check if we've already encountered this vulnerability + if not vd: + vuln_source = vuln.get('source', '').lower() + vd = { + 'cve': vuln_cve, + 'source': 'NVD' if vuln_source == 'nvd' else 'GitHub Advisories', + 'url': f'https://nvd.nist.gov/vuln/detail/{vuln_cve}' + if vuln_source == 'nvd' + else f'https://github.com/advisories/{vuln_id}', + 'severity': self._sev_lookup(vuln.get('severity', 'unknown').lower()), + 'affects': set(), + } + vd.get('affects').add(purl) + vdx[vuln_id] = vd if cdx.get(purl): self.print_debug(f'Component {purl} already stored: {cdx.get(purl)}') continue - fd = {} for field in ['id', 'vendor', 'component', 'version', 'latest']: fd[field] = d.get(field) licenses = d.get('licenses') fdl = [] - for lic in licenses: - fdl.append({'id': lic.get("name")}) + if licenses: + for lic in licenses: + name = lic.get('name') + source = lic.get('source') + if source not in ('component_declared', 'license_file', 'file_header'): + continue + fdl.append({'id': name}) fd['licenses'] = fdl cdx[purl] = fd - return cdx + # self.print_stderr(f'VD: {vdx}') + # self.print_stderr(f'CDX: {cdx}') + return cdx, vdx def produce_from_file(self, json_file: str, output_file: str = None) -> bool: """ @@ -132,57 +180,111 @@ def produce_from_file(self, json_file: str, output_file: str = None) -> bool: success = self.produce_from_str(f.read(), output_file) return success - def produce_from_json(self, data: json, output_file: str = None) -> bool: + def produce_from_json(self, data: dict, output_file: str = None, print_output: bool = True) -> tuple[bool, dict]: # noqa: PLR0912 """ - Produce the CycloneDX output from the input JSON object - :param data: JSON object - :param output_file: Output file (optional) - :return: True if successful, False otherwise + Produce the CycloneDX output from the raw scan results input data + + Args: + data (dict): JSON object + output_file (str, optional): Output file (optional). Defaults to None. + print_output (bool, optional): Print/write output. Defaults to True. + + Returns: + bool: True if successful, False otherwise + json: The CycloneDX output """ - cdx = self.parse(data) - if not cdx: + cdx, vdx = self.parse(data) + if cdx is None: self.print_stderr('ERROR: No CycloneDX data returned for the JSON string provided.') - return False - md5hex = hashlib.md5(f'{time.time()}'.encode('utf-8')).hexdigest() + return False, {} + if len(cdx) == 0: + self.print_msg('Warning: Empty scan results - generating minimal CycloneDX SBOM with no components.') + self._spdx.load_license_data() # Load SPDX license name data for later reference + # + # Using CDX version 1.4: https://cyclonedx.org/docs/1.4/json/ + # Validate using: https://github.com/CycloneDX/cyclonedx-cli + # cyclonedx-cli validate --input-format json --input-version v1_4 --fail-on-errors --input-file cdx.json + # data = { 'bomFormat': 'CycloneDX', - 'specVersion': '1.2', - 'serialNumber': f'scanoss:SCANOSS-PY - SCANOSS CLI-{md5hex}', - 'version': '1', - 'components': [] + 'specVersion': '1.4', + 'serialNumber': f'urn:uuid:{uuid.uuid4()}', + 'version': 1, + 'metadata': { + 'timestamp': datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'), + 'tools': [ + { + 'vendor': 'SCANOSS', + 'name': 'scanoss-py', + 'version': __version__, + } + ], + 'component': {'type': 'application', 'name': 'NOASSERTION', 'version': 'NOASSERTION'}, + }, + 'components': [], + 'vulnerabilities': [], } for purl in cdx: comp = cdx.get(purl) lic_text = [] licenses = comp.get('licenses') if licenses: - for lic in licenses: - lic_text.append({'license': {'id': lic.get('id')}}) - m_type = 'Snippet' if comp.get('id') == 'snippet' else 'Library' - data['components'].append({ - 'type': m_type, + lic_set = set() + for lic in licenses: # Get a unique set of licenses + lc_id = lic.get('id') + if not lc_id: + continue + spdx_id = self._spdx.get_spdx_license_id(lc_id) + lic_set.add(spdx_id if spdx_id else lc_id) + for lc_id in lic_set: # Store licenses for later inclusion + spdx_id = self._spdx.get_spdx_license_id(lc_id) + if not spdx_id: + lic_text.append({'license': {'name': lc_id}}) # Not an SPDX license, so store it by name + else: + lic_text.append({'license': {'id': spdx_id}}) + c_data = { + 'type': 'library', 'name': comp.get('component'), 'publisher': comp.get('vendor', ''), 'version': comp.get('version'), 'purl': purl, - 'licenses': lic_text - # 'licenses': [{ - # 'license': { - # 'id': comp.get('license') - # } - # }] - }) + 'bom-ref': purl, + 'licenses': lic_text, + } + cpe = comp.get('cpe', '') + if cpe and cpe != '': + c_data['cpe'] = cpe + data['components'].append(c_data) # End for loop - file = sys.stdout - if not output_file and self.output_file: - output_file = self.output_file - if output_file: - file = open(output_file, 'w') - print(json.dumps(data, indent=2), file=file) - if output_file: - file.close() + if vdx: + for vuln_id in vdx: + vulns = vdx.get(vuln_id) + if not vulns: + continue + v_source = vulns.get('source') + affects = [] + for purl in vulns.get('affects'): + affects.append({'ref': purl}) + vd = { + 'id': vuln_id, + 'source': {'name': v_source, 'url': vulns.get('url')}, + 'ratings': [{'severity': vulns.get('severity', 'unknown')}], + 'affects': affects, + } + data['vulnerabilities'].append(vd) + # End for loop - return True + if print_output: + file = sys.stdout + if not output_file and self.output_file: + output_file = self.output_file + if output_file: + file = open(output_file, 'w') + print(json.dumps(data, indent=2), file=file) + if output_file: + file.close() + + return True, data def produce_from_str(self, json_str: str, output_file: str = None) -> bool: """ @@ -199,7 +301,145 @@ def produce_from_str(self, json_str: str, output_file: str = None) -> bool: except Exception as e: self.print_stderr(f'ERROR: Problem parsing input JSON: {e}') return False - return self.produce_from_json(data, output_file) + success, _ = self.produce_from_json(data, output_file) + return success + + def _normalize_vulnerability_id(self, vuln: dict) -> tuple[str, str]: + """ + Normalize vulnerability ID and CVE from different possible field names. + Returns tuple of (vuln_id, vuln_cve). + """ + vuln_id = vuln.get('ID', '') or vuln.get('id', '') + vuln_cve = vuln.get('CVE', '') or vuln.get('cve', '') + + # Skip CPE entries, use CVE if available + if vuln_id.upper().startswith('CPE:') and vuln_cve: + vuln_id = vuln_cve + + return vuln_id, vuln_cve + + def _create_vulnerability_entry(self, vuln_id: str, vuln: dict, vuln_cve: str, purl: str) -> dict: + """ + Create a new vulnerability entry for CycloneDX format. + """ + vuln_source = vuln.get('source', '').lower() + return { + 'id': vuln_id, + 'source': { + 'name': 'NVD' if vuln_source == 'nvd' else 'GitHub Advisories', + 'url': f'https://nvd.nist.gov/vuln/detail/{vuln_cve}' + if vuln_source == 'nvd' + else f'https://github.com/advisories/{vuln_id}', + }, + 'ratings': [{'severity': self._sev_lookup(vuln.get('severity', 'unknown').lower())}], + 'affects': [{'ref': purl}], + } + + def append_vulnerabilities(self, cdx_dict: dict, vulnerabilities_data: dict, purl: str) -> dict: + """ + Append vulnerabilities to an existing CycloneDX dictionary + + Args: + cdx_dict (dict): The existing CycloneDX dictionary + vulnerabilities_data (dict): The vulnerabilities data from get_vulnerabilities_json + purl (str): The PURL of the component these vulnerabilities affect + + Returns: + dict: The updated CycloneDX dictionary with vulnerabilities appended + """ + if not cdx_dict or not vulnerabilities_data: + return cdx_dict + + if 'vulnerabilities' not in cdx_dict: + cdx_dict['vulnerabilities'] = [] + + # Extract vulnerabilities from the response + vulns_list = vulnerabilities_data.get('components', []) + if not vulns_list: + return cdx_dict + + vuln_items = vulns_list[0].get('vulnerabilities', []) + + for vuln in vuln_items: + vuln_id, vuln_cve = self._normalize_vulnerability_id(vuln) + + # Skip empty IDs or CPE-only entries + if not vuln_id or vuln_id.upper().startswith('CPE:'): + continue + + # Check if vulnerability already exists + existing_vuln = next((v for v in cdx_dict['vulnerabilities'] if v.get('id') == vuln_id), None) + + if existing_vuln: + # Add this PURL to the affects list if not already present + if not any(ref.get('ref') == purl for ref in existing_vuln.get('affects', [])): + existing_vuln['affects'].append({'ref': purl}) + else: + # Create new vulnerability entry + cdx_dict['vulnerabilities'].append(self._create_vulnerability_entry(vuln_id, vuln, vuln_cve, purl)) + + return cdx_dict + + @staticmethod + def _sev_lookup(value: str): + """ + Lookup the given severity and return a CycloneDX valid version + :param value: severity to lookup + :return: CycloneDX severity + """ + return { + 'critical': 'critical', + 'high': 'high', + 'medium': 'medium', + 'moderate': 'medium', + 'low': 'low', + 'info': 'info', + 'none': 'none', + 'unknown': 'unknown', + }.get(value, 'unknown') + + def is_cyclonedx_json(self, json_string: str) -> bool: + """ + Validate if the given JSON string is a valid CycloneDX JSON string + + Args: + json_string (str): JSON string to validate + Returns: + bool: True if the JSON string is valid, False otherwise + """ + try: + cdx_json_validator = JsonValidator(SchemaVersion.V1_6) + json_validation_errors = cdx_json_validator.validate_str(json_string) + if json_validation_errors: + self.print_stderr(f'ERROR: Problem parsing input JSON: {json_validation_errors}') + return False + return True + except Exception as e: + self.print_stderr(f'ERROR: Problem parsing input JSON: {e}') + return False + + def get_purls_request_from_cdx(self, cdx_dict: dict, field: str = 'purls') -> dict: + """ + Get the list of PURL requests (purl + requirement) from the given CDX dictionary + + Args: + cdx_dict (dict): CDX dictionary to parse + field (str): Field to extract from the CDX dictionary + Returns: + list[dict]: List of PURL requests (purl + requirement) + """ + components = cdx_dict.get('components', []) + parsed_purls = [] + for component in components: + version = component.get('version') + if version: + parsed_purls.append({'purl': component.get('purl'), 'requirement': version}) + else: + parsed_purls.append({'purl': component.get('purl')}) + purl_request = {field: parsed_purls} + return purl_request + + # # End of CycloneDX Class # diff --git a/src/scanoss/data/osadl-copyleft.json b/src/scanoss/data/osadl-copyleft.json new file mode 100644 index 00000000..6cc2e1a7 --- /dev/null +++ b/src/scanoss/data/osadl-copyleft.json @@ -0,0 +1,133 @@ +{ + "title": "OSADL Open Source License Obligations Checklist (https:\/\/www.osadl.org\/Checklists)", + "license": "Creative Commons Attribution 4.0 International license (CC-BY-4.0)", + "attribution": "A project by the Open Source Automation Development Lab (OSADL) eG. For further information about the project see the description at www.osadl.org\/checklists.", + "copyright": "(C) 2017 - 2024 Open Source Automation Development Lab (OSADL) eG and contributors, info@osadl.org", + "disclaimer": "The checklists and particularly the copyleft data have been assembled with maximum diligence and care; however, the authors do not warrant nor can be held liable in any way for its correctness, usefulness, merchantibility or fitness for a particular purpose as far as permissible by applicable law. Anyone who uses the information does this on his or her sole responsibility. For any individual legal advice, it is recommended to contact a lawyer.", + "timeformat": "%Y-%m-%dT%H:%M:%S%z", + "timestamp": "2025-10-30T11:23:00+0000", + "copyleft": + { + "0BSD": "No", + "AFL-2.0": "No", + "AFL-2.1": "No", + "AFL-3.0": "No", + "AGPL-3.0-only": "Yes", + "AGPL-3.0-or-later": "Yes", + "Apache-1.0": "No", + "Apache-1.1": "No", + "Apache-2.0": "No", + "APSL-2.0": "Yes (restricted)", + "Artistic-1.0": "No", + "Artistic-1.0-Perl": "No", + "Artistic-2.0": "No", + "Bitstream-Vera": "No", + "blessing": "No", + "BlueOak-1.0.0": "No", + "BSD-1-Clause": "No", + "BSD-2-Clause": "No", + "BSD-2-Clause-Patent": "No", + "BSD-3-Clause": "No", + "BSD-3-Clause-Open-MPI": "No", + "BSD-4-Clause": "No", + "BSD-4-Clause-UC": "No", + "BSD-4.3TAHOE": "No", + "BSD-Source-Code": "No", + "BSL-1.0": "No", + "bzip2-1.0.5": "No", + "bzip2-1.0.6": "No", + "CC-BY-2.5": "No", + "CC-BY-3.0": "No", + "CDDL-1.0": "Yes (restricted)", + "CDDL-1.1": "Yes (restricted)", + "CPL-1.0": "Yes", + "curl": "No", + "ECL-1.0": "No", + "ECL-2.0": "No", + "EFL-2.0": "No", + "EPL-1.0": "Yes", + "EPL-2.0": "Yes (restricted)", + "EUPL-1.1": "Yes", + "EUPL-1.2": "Yes", + "FSFAP": "No", + "FSFUL": "No", + "FSFULLR": "No", + "FSFULLRWD": "No", + "FTL": "No", + "GPL-1.0-only": "Yes", + "GPL-1.0-or-later": "Yes", + "GPL-2.0-only": "Yes", + "GPL-2.0-only WITH Classpath-exception-2.0": "Yes (restricted)", + "GPL-2.0-or-later": "Yes", + "GPL-3.0-only": "Yes", + "GPL-3.0-or-later": "Yes", + "HPND": "No", + "IBM-pibs": "No", + "ICU": "No", + "IJG": "No", + "ImageMagick": "No", + "Info-ZIP": "No", + "IPL-1.0": "Yes", + "ISC": "No", + "JasPer-2.0": "No", + "LGPL-2.0-only": "Yes (restricted)", + "LGPL-2.0-or-later": "Yes (restricted)", + "LGPL-2.1-only": "Yes (restricted)", + "LGPL-2.1-or-later": "Yes (restricted)", + "LGPL-3.0-only": "Yes (restricted)", + "LGPL-3.0-or-later": "Yes (restricted)", + "Libpng": "No", + "libpng-2.0": "No", + "libtiff": "No", + "LicenseRef-scancode-bsla-no-advert": "No", + "LicenseRef-scancode-info-zip-2003-05": "No", + "LicenseRef-scancode-ppp": "No", + "Minpack": "No", + "MirOS": "No", + "MIT": "No", + "MIT-0": "No", + "MIT-CMU": "No", + "MPL-1.1": "Yes (restricted)", + "MPL-2.0": "Yes (restricted)", + "MPL-2.0-no-copyleft-exception": "Yes (restricted)", + "MS-PL": "Questionable", + "MS-RL": "Yes (restricted)", + "NBPL-1.0": "No", + "NCSA": "No", + "NTP": "No", + "OFL-1.1": "Yes (restricted)", + "OGC-1.0": "No", + "OLDAP-2.8": "No", + "OpenSSL": "Questionable", + "OSL-3.0": "Yes", + "PHP-3.01": "No", + "PostgreSQL": "No", + "PSF-2.0": "No", + "Python-2.0": "No", + "Qhull": "No", + "RSA-MD": "No", + "Saxpath": "No", + "SGI-B-2.0": "No", + "Sleepycat": "Yes", + "SMLNJ": "No", + "Spencer-86": "No", + "SSH-OpenSSH": "No", + "SSH-short": "No", + "SunPro": "No", + "Ubuntu-font-1.0": "Yes (restricted)", + "Unicode-3.0": "No", + "Unicode-DFS-2015": "No", + "Unicode-DFS-2016": "No", + "Unlicense": "No", + "UPL-1.0": "No", + "W3C": "No", + "W3C-19980720": "No", + "W3C-20150513": "No", + "WTFPL": "No", + "X11": "No", + "XFree86-1.1": "No", + "Zlib": "No", + "zlib-acknowledgement": "No", + "ZPL-2.0": "No" + } +} \ No newline at end of file diff --git a/src/scanoss/data/scanoss-settings-schema.json b/src/scanoss/data/scanoss-settings-schema.json new file mode 100644 index 00000000..85c551ab --- /dev/null +++ b/src/scanoss/data/scanoss-settings-schema.json @@ -0,0 +1,359 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Scanoss Settings", + "type": "object", + "properties": { + "self": { + "type": "object", + "description": "Description of the project under analysis", + "properties": { + "name": { + "type": "string", + "description": "Name of the project" + }, + "license": { + "type": "string", + "description": "License of the project" + }, + "description": { + "type": "string", + "description": "Description of the project" + } + } + }, + "settings": { + "type": "object", + "description": "Scan settings and other configurations", + "properties": { + "skip": { + "type": "object", + "description": "Set of rules to skip files from fingerprinting and scanning", + "properties": { + "patterns": { + "type": "object", + "properties": { + "scanning": { + "type": "array", + "description": "List of glob patterns to skip files from scanning", + "items": { + "type": "string", + "examples": [ + "path/to/folder", + "path/to/folder/**", + "path/to/folder/**/*", + "path/to/file.c", + "path/to/another/file.py", + "**/*.ts", + "**/*.json" + ] + }, + "uniqueItems": true + }, + "fingerprinting": { + "type": "array", + "description": "List of glob patterns to skip files from fingerprinting", + "items": { + "type": "string", + "examples": [ + "path/to/folder", + "path/to/folder/**", + "path/to/folder/**/*", + "path/to/file.c", + "path/to/another/file.py", + "**/*.ts", + "**/*.json" + ] + }, + "uniqueItems": true + }, + "dependencies": { + "type": "array", + "description": "List of glob patterns to skip dependency files from dependency analysis", + "items": { + "type": "string", + "examples": [ + "vendor/**", + "third_party/", + "node_modules/**" + ] + }, + "uniqueItems": true + } + } + }, + "sizes": { + "type": "object", + "description": "Set of rules to skip files based on their size.", + "properties": { + "scanning": { + "type": "array", + "items": { + "type": "object", + "properties": { + "patterns": { + "type": "array", + "description": "List of glob patterns to apply the min/max size rule", + "items": { + "type": "string", + "examples": [ + "path/to/folder", + "path/to/folder/**", + "path/to/folder/**/*", + "path/to/file.c", + "path/to/another/file.py", + "**/*.ts", + "**/*.json" + ] + } + }, + "min": { + "type": "integer", + "description": "Minimum size of the file in bytes" + }, + "max": { + "type": "integer", + "description": "Maximum size of the file in bytes" + } + } + } + }, + "fingerprinting": { + "type": "array", + "items": { + "type": "object", + "properties": { + "patterns": { + "type": "array", + "description": "List of glob patterns to apply the min/max size rule", + "items": { + "type": "string" + }, + "examples": [ + "path/to/folder", + "path/to/folder/**", + "path/to/folder/**/*", + "path/to/file.c", + "path/to/another/file.py", + "**/*.ts", + "**/*.json" + ] + }, + "min": { + "type": "integer", + "description": "Minimum size of the file in bytes" + }, + "max": { + "type": "integer", + "description": "Maximum size of the file in bytes" + } + } + } + } + } + } + } + }, + "file_snippet": { + "type": "object", + "description": "File snippet scanning configuration", + "properties": { + "proxy": { + "type": "object", + "description": "Proxy configuration for file snippet requests", + "properties": { + "host": { + "type": "string", + "description": "Proxy host URL" + } + } + }, + "http_config": { + "type": "object", + "description": "HTTP configuration for file snippet requests", + "properties": { + "base_uri": { + "type": "string", + "description": "Base URI for file snippet API requests" + }, + "ignore_cert_errors": { + "type": "boolean", + "description": "Whether to ignore certificate errors" + } + } + }, + "ranking_enabled": { + "type": ["boolean", "null"], + "description": "Enable/disable ranking", + "default": null + }, + "ranking_threshold": { + "type": ["integer", "null"], + "description": "Ranking threshold value. A value of -1 defers to server configuration", + "minimum": -1, + "maximum": 10, + "default": 0 + }, + "min_snippet_hits": { + "type": "integer", + "description": "Minimum snippet hits required", + "minimum": 0, + "default": 0 + }, + "min_snippet_lines": { + "type": "integer", + "description": "Minimum snippet lines required", + "minimum": 0, + "default": 0 + }, + "honour_file_exts": { + "type": ["boolean", "null"], + "description": "Ignores file extensions. When not set, defers to server configuration.", + "default": true + }, + "dependency_analysis": { + "type": "boolean", + "description": "Enable dependency analysis" + }, + "skip_headers": { + "type": "boolean", + "description": "Skip license headers, comments and imports at the beginning of files", + "default": false + }, + "skip_headers_limit": { + "type": "integer", + "description": "Maximum number of lines to skip when filtering headers", + "default": 0 + } + } + }, + "hpfm": { + "type": "object", + "description": "HPFM (High Precision Folder Matching) configuration", + "properties": { + "ranking_enabled": { + "type": "boolean", + "description": "Enable ranking for HPFM" + }, + "ranking_threshold": { + "type": ["integer", "null"], + "description": "Ranking threshold value. A value of -1 defers to server configuration", + "minimum": -1, + "maximum": 99, + "default": 0 + } + } + }, + "container": { + "type": "object", + "description": "Container scanning configuration" + } + } + }, + "bom": { + "type": "object", + "description": "BOM Rules: Set of rules that will be used to modify the BOM before and after the scan is completed", + "properties": { + "include": { + "type": "array", + "description": "Set of rules to be added as context when scanning. This list will be sent as payload to the API.", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "File or folder path. Paths ending with '/' are treated as folder rules and match all files under that directory.", + "examples": ["src/main.c", "src/vendor/"] + }, + "purl": { + "type": "string", + "description": "Package URL to be used to match the component", + "examples": [ + "pkg:npm/vue@2.6.12", + "pkg:golang/github.com/golang/go@1.17.3" + ] + }, + "comment": { + "type": "string", + "description": "Additional notes or comments" + } + }, + "uniqueItems": true, + "required": ["purl"] + } + }, + "remove": { + "type": "array", + "description": "Set of rules that will remove files from the results file after the scan is completed.", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "File or folder path. Paths ending with '/' are treated as folder rules and match all files under that directory.", + "examples": ["src/main.c", "src/vendor/"] + }, + "purl": { + "type": "string", + "description": "Package URL", + "examples": [ + "pkg:npm/vue@2.6.12", + "pkg:golang/github.com/golang/go@1.17.3" + ] + }, + "comment": { + "type": "string", + "description": "Additional notes or comments" + } + }, + "anyOf": [ + {"required": ["purl"]}, + {"required": ["path"]} + ], + "uniqueItems": true + } + }, + "replace": { + "type": "array", + "description": "Set of rules that will replace components with the specified one after the scan is completed.", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "File or folder path. Paths ending with '/' are treated as folder rules and match all files under that directory.", + "examples": ["src/main.c", "src/vendor/"] + }, + "purl": { + "type": "string", + "description": "Package URL to replace", + "examples": [ + "pkg:npm/vue@2.6.12", + "pkg:golang/github.com/golang/go@1.17.3" + ] + }, + "comment": { + "type": "string", + "description": "Additional notes or comments" + }, + "license": { + "type": "string", + "description": "License of the component. Should be a valid SPDX license expression", + "examples": ["MIT", "Apache-2.0"] + }, + "replace_with": { + "type": "string", + "description": "Package URL to replace with", + "examples": [ + "pkg:npm/vue@2.6.12", + "pkg:golang/github.com/golang/go@1.17.3" + ] + } + }, + "uniqueItems": true, + "required": ["purl", "replace_with"] + } + } + } + } + } +} diff --git a/src/scanoss/data/spdx-exceptions.json b/src/scanoss/data/spdx-exceptions.json index f9cef8f5..35e36029 100644 --- a/src/scanoss/data/spdx-exceptions.json +++ b/src/scanoss/data/spdx-exceptions.json @@ -1,77 +1,80 @@ { - "licenseListVersion": "f9911cd", + "licenseListVersion": "166e97c", "exceptions": [ { - "reference": "./OCCT-exception-1.0.json", - "isDeprecatedLicenseId": false, - "detailsUrl": "./OCCT-exception-1.0.html", - "referenceNumber": 1, - "name": "Open CASCADE Exception 1.0", - "licenseExceptionId": "OCCT-exception-1.0", - "seeAlso": [ - "http://www.opencascade.com/content/licensing" - ] - }, - { - "reference": "./Libtool-exception.json", + "reference": "./389-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./Libtool-exception.html", - "referenceNumber": 2, - "name": "Libtool Exception", - "licenseExceptionId": "Libtool-exception", + "detailsUrl": "./389-exception.html", + "referenceNumber": 23, + "name": "389 Directory Server Exception", + "licenseExceptionId": "389-exception", "seeAlso": [ - "http://git.savannah.gnu.org/cgit/libtool.git/tree/m4/libtool.m4" + "http://directory.fedoraproject.org/wiki/GPL_Exception_License_Text", + "https://web.archive.org/web/20080828121337/http://directory.fedoraproject.org/wiki/GPL_Exception_License_Text" ] }, { - "reference": "./gnu-javamail-exception.json", + "reference": "./Autoconf-exception-2.0.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./gnu-javamail-exception.html", - "referenceNumber": 3, - "name": "GNU JavaMail exception", - "licenseExceptionId": "gnu-javamail-exception", + "detailsUrl": "./Autoconf-exception-2.0.html", + "referenceNumber": 13, + "name": "Autoconf exception 2.0", + "licenseExceptionId": "Autoconf-exception-2.0", "seeAlso": [ - "http://www.gnu.org/software/classpathx/javamail/javamail.html" + "http://ac-archive.sourceforge.net/doc/copyright.html", + "http://ftp.gnu.org/gnu/autoconf/autoconf-2.59.tar.gz" ] }, { - "reference": "./SHL-2.1.json", + "reference": "./Autoconf-exception-3.0.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./SHL-2.1.html", - "referenceNumber": 4, - "name": "Solderpad Hardware License v2.1", - "licenseExceptionId": "SHL-2.1", + "detailsUrl": "./Autoconf-exception-3.0.html", + "referenceNumber": 2, + "name": "Autoconf exception 3.0", + "licenseExceptionId": "Autoconf-exception-3.0", "seeAlso": [ - "https://solderpad.org/licenses/SHL-2.1/" + "http://www.gnu.org/licenses/autoconf-exception-3.0.html" ] }, { - "reference": "./Linux-syscall-note.json", + "reference": "./Bison-exception-2.2.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./Linux-syscall-note.html", - "referenceNumber": 5, - "name": "Linux Syscall Note", - "licenseExceptionId": "Linux-syscall-note", + "detailsUrl": "./Bison-exception-2.2.html", + "referenceNumber": 35, + "name": "Bison exception 2.2", + "licenseExceptionId": "Bison-exception-2.2", "seeAlso": [ - "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/COPYING" + "http://git.savannah.gnu.org/cgit/bison.git/tree/data/yacc.c?id\u003d193d7c7054ba7197b0789e14965b739162319b5e#n141" ] }, { "reference": "./Bootloader-exception.json", "isDeprecatedLicenseId": false, "detailsUrl": "./Bootloader-exception.html", - "referenceNumber": 6, + "referenceNumber": 25, "name": "Bootloader Distribution Exception", "licenseExceptionId": "Bootloader-exception", "seeAlso": [ "https://github.com/pyinstaller/pyinstaller/blob/develop/COPYING.txt" ] }, + { + "reference": "./Classpath-exception-2.0.json", + "isDeprecatedLicenseId": false, + "detailsUrl": "./Classpath-exception-2.0.html", + "referenceNumber": 27, + "name": "Classpath exception 2.0", + "licenseExceptionId": "Classpath-exception-2.0", + "seeAlso": [ + "http://www.gnu.org/software/classpath/license.html", + "https://fedoraproject.org/wiki/Licensing/GPL_Classpath_Exception" + ] + }, { "reference": "./CLISP-exception-2.0.json", "isDeprecatedLicenseId": false, "detailsUrl": "./CLISP-exception-2.0.html", - "referenceNumber": 7, + "referenceNumber": 20, "name": "CLISP exception 2.0", "licenseExceptionId": "CLISP-exception-2.0", "seeAlso": [ @@ -79,315 +82,324 @@ ] }, { - "reference": "./GCC-exception-3.1.json", + "reference": "./DigiRule-FOSS-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./GCC-exception-3.1.html", - "referenceNumber": 8, - "name": "GCC Runtime Library exception 3.1", - "licenseExceptionId": "GCC-exception-3.1", + "detailsUrl": "./DigiRule-FOSS-exception.html", + "referenceNumber": 34, + "name": "DigiRule FOSS License Exception", + "licenseExceptionId": "DigiRule-FOSS-exception", "seeAlso": [ - "http://www.gnu.org/licenses/gcc-exception-3.1.html" + "http://www.digirulesolutions.com/drupal/foss" ] }, { - "reference": "./OpenJDK-assembly-exception-1.0.json", + "reference": "./eCos-exception-2.0.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./OpenJDK-assembly-exception-1.0.html", - "referenceNumber": 9, - "name": "OpenJDK Assembly exception 1.0", - "licenseExceptionId": "OpenJDK-assembly-exception-1.0", + "detailsUrl": "./eCos-exception-2.0.html", + "referenceNumber": 44, + "name": "eCos exception 2.0", + "licenseExceptionId": "eCos-exception-2.0", "seeAlso": [ - "http://openjdk.java.net/legal/assembly-exception.html" + "http://ecos.sourceware.org/license-overview.html" ] }, { - "reference": "./WxWindows-exception-3.1.json", + "reference": "./Fawkes-Runtime-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./WxWindows-exception-3.1.html", - "referenceNumber": 10, - "name": "WxWindows Library Exception 3.1", - "licenseExceptionId": "WxWindows-exception-3.1", + "detailsUrl": "./Fawkes-Runtime-exception.html", + "referenceNumber": 7, + "name": "Fawkes Runtime Exception", + "licenseExceptionId": "Fawkes-Runtime-exception", "seeAlso": [ - "http://www.opensource.org/licenses/WXwindows" + "http://www.fawkesrobotics.org/about/license/" ] }, { - "reference": "./eCos-exception-2.0.json", + "reference": "./FLTK-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./eCos-exception-2.0.html", - "referenceNumber": 11, - "name": "eCos exception 2.0", - "licenseExceptionId": "eCos-exception-2.0", + "detailsUrl": "./FLTK-exception.html", + "referenceNumber": 43, + "name": "FLTK exception", + "licenseExceptionId": "FLTK-exception", "seeAlso": [ - "http://ecos.sourceware.org/license-overview.html" + "http://www.fltk.org/COPYING.php" ] }, { - "reference": "./LLVM-exception.json", + "reference": "./Font-exception-2.0.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./LLVM-exception.html", - "referenceNumber": 12, - "name": "LLVM Exception", - "licenseExceptionId": "LLVM-exception", + "detailsUrl": "./Font-exception-2.0.html", + "referenceNumber": 3, + "name": "Font exception 2.0", + "licenseExceptionId": "Font-exception-2.0", "seeAlso": [ - "http://llvm.org/foundation/relicensing/LICENSE.txt" + "http://www.gnu.org/licenses/gpl-faq.html#FontException" ] }, { - "reference": "./GPL-CC-1.0.json", + "reference": "./freertos-exception-2.0.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./GPL-CC-1.0.html", - "referenceNumber": 13, - "name": "GPL Cooperation Commitment 1.0", - "licenseExceptionId": "GPL-CC-1.0", + "detailsUrl": "./freertos-exception-2.0.html", + "referenceNumber": 32, + "name": "FreeRTOS Exception 2.0", + "licenseExceptionId": "freertos-exception-2.0", "seeAlso": [ - "https://github.com/gplcc/gplcc/blob/master/Project/COMMITMENT", - "https://gplcc.github.io/gplcc/Project/README-PROJECT.html" + "https://web.archive.org/web/20060809182744/http://www.freertos.org/a00114.html" ] }, { - "reference": "./mif-exception.json", + "reference": "./GCC-exception-2.0.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./mif-exception.html", - "referenceNumber": 14, - "name": "Macros and Inline Functions Exception", - "licenseExceptionId": "mif-exception", + "detailsUrl": "./GCC-exception-2.0.html", + "referenceNumber": 19, + "name": "GCC Runtime Library exception 2.0", + "licenseExceptionId": "GCC-exception-2.0", "seeAlso": [ - "http://www.scs.stanford.edu/histar/src/lib/cppsup/exception", - "http://dev.bertos.org/doxygen/", - "https://www.threadingbuildingblocks.org/licensing" + "https://gcc.gnu.org/git/?p\u003dgcc.git;a\u003dblob;f\u003dgcc/libgcc1.c;h\u003d762f5143fc6eed57b6797c82710f3538aa52b40b;hb\u003dcb143a3ce4fb417c68f5fa2691a1b1b1053dfba9#l10" ] }, { - "reference": "./Classpath-exception-2.0.json", + "reference": "./GCC-exception-3.1.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./Classpath-exception-2.0.html", - "referenceNumber": 15, - "name": "Classpath exception 2.0", - "licenseExceptionId": "Classpath-exception-2.0", + "detailsUrl": "./GCC-exception-3.1.html", + "referenceNumber": 31, + "name": "GCC Runtime Library exception 3.1", + "licenseExceptionId": "GCC-exception-3.1", "seeAlso": [ - "http://www.gnu.org/software/classpath/license.html", - "https://fedoraproject.org/wiki/Licensing/GPL_Classpath_Exception" + "http://www.gnu.org/licenses/gcc-exception-3.1.html" ] }, { - "reference": "./SHL-2.0.json", + "reference": "./gnu-javamail-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./SHL-2.0.html", - "referenceNumber": 16, - "name": "Solderpad Hardware License v2.0", - "licenseExceptionId": "SHL-2.0", + "detailsUrl": "./gnu-javamail-exception.html", + "referenceNumber": 8, + "name": "GNU JavaMail exception", + "licenseExceptionId": "gnu-javamail-exception", "seeAlso": [ - "https://solderpad.org/licenses/SHL-2.0/" + "http://www.gnu.org/software/classpathx/javamail/javamail.html" ] }, { - "reference": "./Qwt-exception-1.0.json", + "reference": "./GPL-3.0-linking-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./Qwt-exception-1.0.html", - "referenceNumber": 17, - "name": "Qwt exception 1.0", - "licenseExceptionId": "Qwt-exception-1.0", + "detailsUrl": "./GPL-3.0-linking-exception.html", + "referenceNumber": 18, + "name": "GPL-3.0 Linking Exception", + "licenseExceptionId": "GPL-3.0-linking-exception", "seeAlso": [ - "http://qwt.sourceforge.net/qwtlicense.html" + "https://www.gnu.org/licenses/gpl-faq.en.html#GPLIncompatibleLibs" ] }, { - "reference": "./i2p-gpl-java-exception.json", + "reference": "./GPL-3.0-linking-source-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./i2p-gpl-java-exception.html", - "referenceNumber": 18, - "name": "i2p GPL+Java Exception", - "licenseExceptionId": "i2p-gpl-java-exception", + "detailsUrl": "./GPL-3.0-linking-source-exception.html", + "referenceNumber": 30, + "name": "GPL-3.0 Linking Exception (with Corresponding Source)", + "licenseExceptionId": "GPL-3.0-linking-source-exception", "seeAlso": [ - "http://geti2p.net/en/get-involved/develop/licenses#java_exception" + "https://www.gnu.org/licenses/gpl-faq.en.html#GPLIncompatibleLibs", + "https://github.com/mirror/wget/blob/master/src/http.c#L20" ] }, { - "reference": "./Autoconf-exception-3.0.json", + "reference": "./GPL-CC-1.0.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./Autoconf-exception-3.0.html", - "referenceNumber": 19, - "name": "Autoconf exception 3.0", - "licenseExceptionId": "Autoconf-exception-3.0", + "detailsUrl": "./GPL-CC-1.0.html", + "referenceNumber": 45, + "name": "GPL Cooperation Commitment 1.0", + "licenseExceptionId": "GPL-CC-1.0", "seeAlso": [ - "http://www.gnu.org/licenses/autoconf-exception-3.0.html" + "https://github.com/gplcc/gplcc/blob/master/Project/COMMITMENT", + "https://gplcc.github.io/gplcc/Project/README-PROJECT.html" ] }, { - "reference": "./OCaml-LGPL-linking-exception.json", + "reference": "./GStreamer-exception-2005.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./OCaml-LGPL-linking-exception.html", - "referenceNumber": 20, - "name": "OCaml LGPL Linking Exception", - "licenseExceptionId": "OCaml-LGPL-linking-exception", + "detailsUrl": "./GStreamer-exception-2005.html", + "referenceNumber": 28, + "name": "GStreamer Exception (2005)", + "licenseExceptionId": "GStreamer-exception-2005", "seeAlso": [ - "https://caml.inria.fr/ocaml/license.en.html" + "https://gstreamer.freedesktop.org/documentation/frequently-asked-questions/licensing.html?gi-language\u003dc#licensing-of-applications-using-gstreamer" ] }, { - "reference": "./FLTK-exception.json", + "reference": "./GStreamer-exception-2008.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./FLTK-exception.html", - "referenceNumber": 21, - "name": "FLTK exception", - "licenseExceptionId": "FLTK-exception", + "detailsUrl": "./GStreamer-exception-2008.html", + "referenceNumber": 10, + "name": "GStreamer Exception (2008)", + "licenseExceptionId": "GStreamer-exception-2008", "seeAlso": [ - "http://www.fltk.org/COPYING.php" + "https://gstreamer.freedesktop.org/documentation/frequently-asked-questions/licensing.html?gi-language\u003dc#licensing-of-applications-using-gstreamer" ] }, { - "reference": "./GPL-3.0-linking-exception.json", + "reference": "./i2p-gpl-java-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./GPL-3.0-linking-exception.html", - "referenceNumber": 22, - "name": "GPL-3.0 Linking Exception", - "licenseExceptionId": "GPL-3.0-linking-exception", + "detailsUrl": "./i2p-gpl-java-exception.html", + "referenceNumber": 42, + "name": "i2p GPL+Java Exception", + "licenseExceptionId": "i2p-gpl-java-exception", "seeAlso": [ - "https://www.gnu.org/licenses/gpl-faq.en.html#GPLIncompatibleLibs" + "http://geti2p.net/en/get-involved/develop/licenses#java_exception" ] }, { - "reference": "./Universal-FOSS-exception-1.0.json", + "reference": "./KiCad-libraries-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./Universal-FOSS-exception-1.0.html", - "referenceNumber": 23, - "name": "Universal FOSS Exception, Version 1.0", - "licenseExceptionId": "Universal-FOSS-exception-1.0", + "detailsUrl": "./KiCad-libraries-exception.html", + "referenceNumber": 38, + "name": "KiCad Libraries Exception", + "licenseExceptionId": "KiCad-libraries-exception", "seeAlso": [ - "https://oss.oracle.com/licenses/universal-foss-exception/" + "https://www.kicad.org/libraries/license/" ] }, { - "reference": "./LZMA-exception.json", + "reference": "./LGPL-3.0-linking-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./LZMA-exception.html", - "referenceNumber": 24, - "name": "LZMA exception", - "licenseExceptionId": "LZMA-exception", + "detailsUrl": "./LGPL-3.0-linking-exception.html", + "referenceNumber": 16, + "name": "LGPL-3.0 Linking Exception", + "licenseExceptionId": "LGPL-3.0-linking-exception", "seeAlso": [ - "http://nsis.sourceforge.net/Docs/AppendixI.html#I.6" + "https://raw.githubusercontent.com/go-xmlpath/xmlpath/v2/LICENSE", + "https://github.com/goamz/goamz/blob/master/LICENSE", + "https://github.com/juju/errors/blob/master/LICENSE" ] }, { - "reference": "./DigiRule-FOSS-exception.json", + "reference": "./Libtool-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./DigiRule-FOSS-exception.html", - "referenceNumber": 25, - "name": "DigiRule FOSS License Exception", - "licenseExceptionId": "DigiRule-FOSS-exception", + "detailsUrl": "./Libtool-exception.html", + "referenceNumber": 4, + "name": "Libtool Exception", + "licenseExceptionId": "Libtool-exception", "seeAlso": [ - "http://www.digirulesolutions.com/drupal/foss" + "http://git.savannah.gnu.org/cgit/libtool.git/tree/m4/libtool.m4" ] }, { - "reference": "./GPL-3.0-linking-source-exception.json", + "reference": "./Linux-syscall-note.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./GPL-3.0-linking-source-exception.html", - "referenceNumber": 26, - "name": "GPL-3.0 Linking Exception (with Corresponding Source)", - "licenseExceptionId": "GPL-3.0-linking-source-exception", + "detailsUrl": "./Linux-syscall-note.html", + "referenceNumber": 6, + "name": "Linux Syscall Note", + "licenseExceptionId": "Linux-syscall-note", "seeAlso": [ - "https://www.gnu.org/licenses/gpl-faq.en.html#GPLIncompatibleLibs", - "https://github.com/mirror/wget/blob/master/src/http.c#L20" + "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/COPYING" ] }, { - "reference": "./u-boot-exception-2.0.json", + "reference": "./LLVM-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./u-boot-exception-2.0.html", - "referenceNumber": 27, - "name": "U-Boot exception 2.0", - "licenseExceptionId": "u-boot-exception-2.0", + "detailsUrl": "./LLVM-exception.html", + "referenceNumber": 21, + "name": "LLVM Exception", + "licenseExceptionId": "LLVM-exception", "seeAlso": [ - "http://git.denx.de/?p\u003du-boot.git;a\u003dblob;f\u003dLicenses/Exceptions" + "http://llvm.org/foundation/relicensing/LICENSE.txt" ] }, { - "reference": "./GCC-exception-2.0.json", + "reference": "./LZMA-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./GCC-exception-2.0.html", - "referenceNumber": 28, - "name": "GCC Runtime Library exception 2.0", - "licenseExceptionId": "GCC-exception-2.0", + "detailsUrl": "./LZMA-exception.html", + "referenceNumber": 11, + "name": "LZMA exception", + "licenseExceptionId": "LZMA-exception", "seeAlso": [ - "https://gcc.gnu.org/git/?p\u003dgcc.git;a\u003dblob;f\u003dgcc/libgcc1.c;h\u003d762f5143fc6eed57b6797c82710f3538aa52b40b;hb\u003dcb143a3ce4fb417c68f5fa2691a1b1b1053dfba9#l10" + "http://nsis.sourceforge.net/Docs/AppendixI.html#I.6" ] }, { - "reference": "./PS-or-PDF-font-exception-20170817.json", + "reference": "./mif-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./PS-or-PDF-font-exception-20170817.html", - "referenceNumber": 29, - "name": "PS/PDF font exception (2017-08-17)", - "licenseExceptionId": "PS-or-PDF-font-exception-20170817", + "detailsUrl": "./mif-exception.html", + "referenceNumber": 33, + "name": "Macros and Inline Functions Exception", + "licenseExceptionId": "mif-exception", "seeAlso": [ - "https://github.com/ArtifexSoftware/urw-base35-fonts/blob/65962e27febc3883a17e651cdb23e783668c996f/LICENSE" + "http://www.scs.stanford.edu/histar/src/lib/cppsup/exception", + "http://dev.bertos.org/doxygen/", + "https://www.threadingbuildingblocks.org/licensing" ] }, { - "reference": "./LGPL-3.0-linking-exception.json", + "reference": "./Nokia-Qt-exception-1.1.json", + "isDeprecatedLicenseId": true, + "detailsUrl": "./Nokia-Qt-exception-1.1.html", + "referenceNumber": 17, + "name": "Nokia Qt LGPL exception 1.1", + "licenseExceptionId": "Nokia-Qt-exception-1.1", + "seeAlso": [ + "https://www.keepassx.org/dev/projects/keepassx/repository/revisions/b8dfb9cc4d5133e0f09cd7533d15a4f1c19a40f2/entry/LICENSE.NOKIA-LGPL-EXCEPTION" + ] + }, + { + "reference": "./OCaml-LGPL-linking-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./LGPL-3.0-linking-exception.html", - "referenceNumber": 30, - "name": "LGPL-3.0 Linking Exception", - "licenseExceptionId": "LGPL-3.0-linking-exception", + "detailsUrl": "./OCaml-LGPL-linking-exception.html", + "referenceNumber": 12, + "name": "OCaml LGPL Linking Exception", + "licenseExceptionId": "OCaml-LGPL-linking-exception", "seeAlso": [ - "https://raw.githubusercontent.com/go-xmlpath/xmlpath/v2/LICENSE", - "https://github.com/goamz/goamz/blob/master/LICENSE", - "https://github.com/juju/errors/blob/master/LICENSE" + "https://caml.inria.fr/ocaml/license.en.html" ] }, { - "reference": "./Fawkes-Runtime-exception.json", + "reference": "./OCCT-exception-1.0.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./Fawkes-Runtime-exception.html", - "referenceNumber": 31, - "name": "Fawkes Runtime Exception", - "licenseExceptionId": "Fawkes-Runtime-exception", + "detailsUrl": "./OCCT-exception-1.0.html", + "referenceNumber": 1, + "name": "Open CASCADE Exception 1.0", + "licenseExceptionId": "OCCT-exception-1.0", "seeAlso": [ - "http://www.fawkesrobotics.org/about/license/" + "http://www.opencascade.com/content/licensing" ] }, { - "reference": "./Font-exception-2.0.json", + "reference": "./OpenJDK-assembly-exception-1.0.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./Font-exception-2.0.html", - "referenceNumber": 32, - "name": "Font exception 2.0", - "licenseExceptionId": "Font-exception-2.0", + "detailsUrl": "./OpenJDK-assembly-exception-1.0.html", + "referenceNumber": 5, + "name": "OpenJDK Assembly exception 1.0", + "licenseExceptionId": "OpenJDK-assembly-exception-1.0", "seeAlso": [ - "http://www.gnu.org/licenses/gpl-faq.html#FontException" + "http://openjdk.java.net/legal/assembly-exception.html" ] }, { - "reference": "./Swift-exception.json", + "reference": "./openvpn-openssl-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./Swift-exception.html", - "referenceNumber": 33, - "name": "Swift Exception", - "licenseExceptionId": "Swift-exception", + "detailsUrl": "./openvpn-openssl-exception.html", + "referenceNumber": 37, + "name": "OpenVPN OpenSSL Exception", + "licenseExceptionId": "openvpn-openssl-exception", "seeAlso": [ - "https://swift.org/LICENSE.txt", - "https://github.com/apple/swift-package-manager/blob/7ab2275f447a5eb37497ed63a9340f8a6d1e488b/LICENSE.txt#L205" + "http://openvpn.net/index.php/license.html" ] }, { - "reference": "./Bison-exception-2.2.json", + "reference": "./PS-or-PDF-font-exception-20170817.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./Bison-exception-2.2.html", - "referenceNumber": 34, - "name": "Bison exception 2.2", - "licenseExceptionId": "Bison-exception-2.2", + "detailsUrl": "./PS-or-PDF-font-exception-20170817.html", + "referenceNumber": 36, + "name": "PS/PDF font exception (2017-08-17)", + "licenseExceptionId": "PS-or-PDF-font-exception-20170817", "seeAlso": [ - "http://git.savannah.gnu.org/cgit/bison.git/tree/data/yacc.c?id\u003d193d7c7054ba7197b0789e14965b739162319b5e#n141" + "https://github.com/ArtifexSoftware/urw-base35-fonts/blob/65962e27febc3883a17e651cdb23e783668c996f/LICENSE" ] }, { "reference": "./Qt-GPL-exception-1.0.json", "isDeprecatedLicenseId": false, "detailsUrl": "./Qt-GPL-exception-1.0.html", - "referenceNumber": 35, + "referenceNumber": 40, "name": "Qt GPL exception 1.0", "licenseExceptionId": "Qt-GPL-exception-1.0", "seeAlso": [ @@ -398,7 +410,7 @@ "reference": "./Qt-LGPL-exception-1.1.json", "isDeprecatedLicenseId": false, "detailsUrl": "./Qt-LGPL-exception-1.1.html", - "referenceNumber": 36, + "referenceNumber": 29, "name": "Qt LGPL exception 1.1", "licenseExceptionId": "Qt-LGPL-exception-1.1", "seeAlso": [ @@ -406,61 +418,94 @@ ] }, { - "reference": "./Nokia-Qt-exception-1.1.json", - "isDeprecatedLicenseId": true, - "detailsUrl": "./Nokia-Qt-exception-1.1.html", - "referenceNumber": 37, - "name": "Nokia Qt LGPL exception 1.1", - "licenseExceptionId": "Nokia-Qt-exception-1.1", + "reference": "./Qwt-exception-1.0.json", + "isDeprecatedLicenseId": false, + "detailsUrl": "./Qwt-exception-1.0.html", + "referenceNumber": 41, + "name": "Qwt exception 1.0", + "licenseExceptionId": "Qwt-exception-1.0", "seeAlso": [ - "https://www.keepassx.org/dev/projects/keepassx/repository/revisions/b8dfb9cc4d5133e0f09cd7533d15a4f1c19a40f2/entry/LICENSE.NOKIA-LGPL-EXCEPTION" + "http://qwt.sourceforge.net/qwtlicense.html" ] }, { - "reference": "./389-exception.json", + "reference": "./SHL-2.0.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./389-exception.html", - "referenceNumber": 38, - "name": "389 Directory Server Exception", - "licenseExceptionId": "389-exception", + "detailsUrl": "./SHL-2.0.html", + "referenceNumber": 22, + "name": "Solderpad Hardware License v2.0", + "licenseExceptionId": "SHL-2.0", "seeAlso": [ - "http://directory.fedoraproject.org/wiki/GPL_Exception_License_Text" + "https://solderpad.org/licenses/SHL-2.0/" ] }, { - "reference": "./openvpn-openssl-exception.json", + "reference": "./SHL-2.1.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./openvpn-openssl-exception.html", + "detailsUrl": "./SHL-2.1.html", + "referenceNumber": 24, + "name": "Solderpad Hardware License v2.1", + "licenseExceptionId": "SHL-2.1", + "seeAlso": [ + "https://solderpad.org/licenses/SHL-2.1/" + ] + }, + { + "reference": "./Swift-exception.json", + "isDeprecatedLicenseId": false, + "detailsUrl": "./Swift-exception.html", + "referenceNumber": 15, + "name": "Swift Exception", + "licenseExceptionId": "Swift-exception", + "seeAlso": [ + "https://swift.org/LICENSE.txt", + "https://github.com/apple/swift-package-manager/blob/7ab2275f447a5eb37497ed63a9340f8a6d1e488b/LICENSE.txt#L205" + ] + }, + { + "reference": "./u-boot-exception-2.0.json", + "isDeprecatedLicenseId": false, + "detailsUrl": "./u-boot-exception-2.0.html", + "referenceNumber": 14, + "name": "U-Boot exception 2.0", + "licenseExceptionId": "u-boot-exception-2.0", + "seeAlso": [ + "http://git.denx.de/?p\u003du-boot.git;a\u003dblob;f\u003dLicenses/Exceptions" + ] + }, + { + "reference": "./Universal-FOSS-exception-1.0.json", + "isDeprecatedLicenseId": false, + "detailsUrl": "./Universal-FOSS-exception-1.0.html", "referenceNumber": 39, - "name": "OpenVPN OpenSSL Exception", - "licenseExceptionId": "openvpn-openssl-exception", + "name": "Universal FOSS Exception, Version 1.0", + "licenseExceptionId": "Universal-FOSS-exception-1.0", "seeAlso": [ - "http://openvpn.net/index.php/license.html" + "https://oss.oracle.com/licenses/universal-foss-exception/" ] }, { - "reference": "./freertos-exception-2.0.json", + "reference": "./WxWindows-exception-3.1.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./freertos-exception-2.0.html", - "referenceNumber": 40, - "name": "FreeRTOS Exception 2.0", - "licenseExceptionId": "freertos-exception-2.0", + "detailsUrl": "./WxWindows-exception-3.1.html", + "referenceNumber": 9, + "name": "WxWindows Library Exception 3.1", + "licenseExceptionId": "WxWindows-exception-3.1", "seeAlso": [ - "https://web.archive.org/web/20060809182744/http://www.freertos.org/a00114.html" + "http://www.opensource.org/licenses/WXwindows" ] }, { - "reference": "./Autoconf-exception-2.0.json", + "reference": "./x11vnc-openssl-exception.json", "isDeprecatedLicenseId": false, - "detailsUrl": "./Autoconf-exception-2.0.html", - "referenceNumber": 41, - "name": "Autoconf exception 2.0", - "licenseExceptionId": "Autoconf-exception-2.0", + "detailsUrl": "./x11vnc-openssl-exception.html", + "referenceNumber": 26, + "name": "x11vnc OpenSSL Exception", + "licenseExceptionId": "x11vnc-openssl-exception", "seeAlso": [ - "http://ac-archive.sourceforge.net/doc/copyright.html", - "http://ftp.gnu.org/gnu/autoconf/autoconf-2.59.tar.gz" + "https://github.com/LibVNC/x11vnc/blob/master/src/8to24.c#L22" ] } ], - "releaseDate": "2021-11-19" + "releaseDate": "2023-01-02" } \ No newline at end of file diff --git a/src/scanoss/data/spdx-licenses.json b/src/scanoss/data/spdx-licenses.json index 2be785d5..b92b9b46 100644 --- a/src/scanoss/data/spdx-licenses.json +++ b/src/scanoss/data/spdx-licenses.json @@ -1,2534 +1,2463 @@ { - "licenseListVersion": "f9911cd", + "licenseListVersion": "166e97c", "licenses": [ { - "reference": "https://spdx.org/licenses/EUDatagrid.html", + "reference": "https://spdx.org/licenses/0BSD.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/EUDatagrid.json", - "referenceNumber": 0, - "name": "EU DataGrid Software License", - "licenseId": "EUDatagrid", + "detailsUrl": "https://spdx.org/licenses/0BSD.json", + "referenceNumber": 413, + "name": "BSD Zero Clause License", + "licenseId": "0BSD", "seeAlso": [ - "http://eu-datagrid.web.cern.ch/eu-datagrid/license.html", - "https://opensource.org/licenses/EUDatagrid" + "http://landley.net/toybox/license.html", + "https://opensource.org/licenses/0BSD" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/VOSTROM.html", + "reference": "https://spdx.org/licenses/AAL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/VOSTROM.json", - "referenceNumber": 1, - "name": "VOSTROM Public License for Open Source", - "licenseId": "VOSTROM", + "detailsUrl": "https://spdx.org/licenses/AAL.json", + "referenceNumber": 192, + "name": "Attribution Assurance License", + "licenseId": "AAL", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/VOSTROM" + "https://opensource.org/licenses/attribution" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/GFDL-1.3-no-invariants-only.html", + "reference": "https://spdx.org/licenses/Abstyles.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.3-no-invariants-only.json", - "referenceNumber": 2, - "name": "GNU Free Documentation License v1.3 only - no invariants", - "licenseId": "GFDL-1.3-no-invariants-only", + "detailsUrl": "https://spdx.org/licenses/Abstyles.json", + "referenceNumber": 326, + "name": "Abstyles License", + "licenseId": "Abstyles", "seeAlso": [ - "https://www.gnu.org/licenses/fdl-1.3.txt" + "https://fedoraproject.org/wiki/Licensing/Abstyles" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-SA-1.0.html", + "reference": "https://spdx.org/licenses/Adobe-2006.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-1.0.json", - "referenceNumber": 3, - "name": "Creative Commons Attribution Share Alike 1.0 Generic", - "licenseId": "CC-BY-SA-1.0", + "detailsUrl": "https://spdx.org/licenses/Adobe-2006.json", + "referenceNumber": 6, + "name": "Adobe Systems Incorporated Source Code License Agreement", + "licenseId": "Adobe-2006", "seeAlso": [ - "https://creativecommons.org/licenses/by-sa/1.0/legalcode" + "https://fedoraproject.org/wiki/Licensing/AdobeLicense" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-3.0-DE.html", + "reference": "https://spdx.org/licenses/Adobe-Glyph.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-3.0-DE.json", - "referenceNumber": 4, - "name": "Creative Commons Attribution 3.0 Germany", - "licenseId": "CC-BY-3.0-DE", + "detailsUrl": "https://spdx.org/licenses/Adobe-Glyph.json", + "referenceNumber": 330, + "name": "Adobe Glyph List License", + "licenseId": "Adobe-Glyph", "seeAlso": [ - "https://creativecommons.org/licenses/by/3.0/de/legalcode" + "https://fedoraproject.org/wiki/Licensing/MIT#AdobeGlyph" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-SA-2.5.html", + "reference": "https://spdx.org/licenses/ADSL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-2.5.json", - "referenceNumber": 5, - "name": "Creative Commons Attribution Share Alike 2.5 Generic", - "licenseId": "CC-BY-SA-2.5", + "detailsUrl": "https://spdx.org/licenses/ADSL.json", + "referenceNumber": 133, + "name": "Amazon Digital Services License", + "licenseId": "ADSL", "seeAlso": [ - "https://creativecommons.org/licenses/by-sa/2.5/legalcode" + "https://fedoraproject.org/wiki/Licensing/AmazonDigitalServicesLicense" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/HaskellReport.html", + "reference": "https://spdx.org/licenses/AFL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/HaskellReport.json", - "referenceNumber": 6, - "name": "Haskell Language Report License", - "licenseId": "HaskellReport", + "detailsUrl": "https://spdx.org/licenses/AFL-1.1.json", + "referenceNumber": 143, + "name": "Academic Free License v1.1", + "licenseId": "AFL-1.1", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Haskell_Language_Report_License" + "http://opensource.linux-mirror.org/licenses/afl-1.1.txt", + "http://wayback.archive.org/web/20021004124254/http://www.opensource.org/licenses/academic.php" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/SimPL-2.0.html", + "reference": "https://spdx.org/licenses/AFL-1.2.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SimPL-2.0.json", - "referenceNumber": 7, - "name": "Simple Public License 2.0", - "licenseId": "SimPL-2.0", + "detailsUrl": "https://spdx.org/licenses/AFL-1.2.json", + "referenceNumber": 411, + "name": "Academic Free License v1.2", + "licenseId": "AFL-1.2", "seeAlso": [ - "https://opensource.org/licenses/SimPL-2.0" + "http://opensource.linux-mirror.org/licenses/afl-1.2.txt", + "http://wayback.archive.org/web/20021204204652/http://www.opensource.org/licenses/academic.php" ], - "isOsiApproved": true + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/ClArtistic.html", + "reference": "https://spdx.org/licenses/AFL-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/ClArtistic.json", - "referenceNumber": 8, - "name": "Clarified Artistic License", - "licenseId": "ClArtistic", + "detailsUrl": "https://spdx.org/licenses/AFL-2.0.json", + "referenceNumber": 263, + "name": "Academic Free License v2.0", + "licenseId": "AFL-2.0", "seeAlso": [ - "http://gianluca.dellavedova.org/2011/01/03/clarified-artistic-license/", - "http://www.ncftp.com/ncftp/doc/LICENSE.txt" + "http://wayback.archive.org/web/20060924134533/http://www.opensource.org/licenses/afl-2.0.txt" ], - "isOsiApproved": false, + "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Nunit.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/Nunit.json", - "referenceNumber": 9, - "name": "Nunit License", - "licenseId": "Nunit", + "reference": "https://spdx.org/licenses/AFL-2.1.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/AFL-2.1.json", + "referenceNumber": 363, + "name": "Academic Free License v2.1", + "licenseId": "AFL-2.1", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Nunit" + "http://opensource.linux-mirror.org/licenses/afl-2.1.txt" ], - "isOsiApproved": false, + "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/SMPPL.html", + "reference": "https://spdx.org/licenses/AFL-3.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SMPPL.json", - "referenceNumber": 10, - "name": "Secure Messaging Protocol Public License", - "licenseId": "SMPPL", + "detailsUrl": "https://spdx.org/licenses/AFL-3.0.json", + "referenceNumber": 147, + "name": "Academic Free License v3.0", + "licenseId": "AFL-3.0", "seeAlso": [ - "https://github.com/dcblake/SMP/blob/master/Documentation/License.txt" + "http://www.rosenlaw.com/AFL3.0.htm", + "https://opensource.org/licenses/afl-3.0" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/LGPL-2.0+.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/LGPL-2.0+.json", - "referenceNumber": 11, - "name": "GNU Library General Public License v2 or later", - "licenseId": "LGPL-2.0+", + "reference": "https://spdx.org/licenses/Afmparse.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/Afmparse.json", + "referenceNumber": 468, + "name": "Afmparse License", + "licenseId": "Afmparse", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/lgpl-2.0-standalone.html" + "https://fedoraproject.org/wiki/Licensing/Afmparse" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CUA-OPL-1.0.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CUA-OPL-1.0.json", - "referenceNumber": 12, - "name": "CUA Office Public License v1.0", - "licenseId": "CUA-OPL-1.0", + "reference": "https://spdx.org/licenses/AGPL-1.0.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/AGPL-1.0.json", + "referenceNumber": 444, + "name": "Affero General Public License v1.0", + "licenseId": "AGPL-1.0", "seeAlso": [ - "https://opensource.org/licenses/CUA-OPL-1.0" + "http://www.affero.org/oagpl.html" ], - "isOsiApproved": true + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/OLDAP-1.3.html", + "reference": "https://spdx.org/licenses/AGPL-1.0-only.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OLDAP-1.3.json", - "referenceNumber": 13, - "name": "Open LDAP Public License v1.3", - "licenseId": "OLDAP-1.3", + "detailsUrl": "https://spdx.org/licenses/AGPL-1.0-only.json", + "referenceNumber": 458, + "name": "Affero General Public License v1.0 only", + "licenseId": "AGPL-1.0-only", "seeAlso": [ - "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003de5f8117f0ce088d0bd7a8e18ddf37eaa40eb09b1" + "http://www.affero.org/oagpl.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Giftware.html", + "reference": "https://spdx.org/licenses/AGPL-1.0-or-later.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Giftware.json", - "referenceNumber": 14, - "name": "Giftware License", - "licenseId": "Giftware", + "detailsUrl": "https://spdx.org/licenses/AGPL-1.0-or-later.json", + "referenceNumber": 97, + "name": "Affero General Public License v1.0 or later", + "licenseId": "AGPL-1.0-or-later", "seeAlso": [ - "http://liballeg.org/license.html#allegro-4-the-giftware-license" + "http://www.affero.org/oagpl.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BitTorrent-1.1.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BitTorrent-1.1.json", - "referenceNumber": 15, - "name": "BitTorrent Open Source License v1.1", - "licenseId": "BitTorrent-1.1", + "reference": "https://spdx.org/licenses/AGPL-3.0.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/AGPL-3.0.json", + "referenceNumber": 385, + "name": "GNU Affero General Public License v3.0", + "licenseId": "AGPL-3.0", "seeAlso": [ - "http://directory.fsf.org/wiki/License:BitTorrentOSL1.1" + "https://www.gnu.org/licenses/agpl.txt", + "https://opensource.org/licenses/AGPL-3.0" ], - "isOsiApproved": false, + "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/ISC.html", + "reference": "https://spdx.org/licenses/AGPL-3.0-only.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/ISC.json", - "referenceNumber": 16, - "name": "ISC License", - "licenseId": "ISC", + "detailsUrl": "https://spdx.org/licenses/AGPL-3.0-only.json", + "referenceNumber": 439, + "name": "GNU Affero General Public License v3.0 only", + "licenseId": "AGPL-3.0-only", "seeAlso": [ - "https://www.isc.org/licenses/", - "https://www.isc.org/downloads/software-support-policy/isc-license/", - "https://opensource.org/licenses/ISC" + "https://www.gnu.org/licenses/agpl.txt", + "https://opensource.org/licenses/AGPL-3.0" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/LPPL-1.1.html", + "reference": "https://spdx.org/licenses/AGPL-3.0-or-later.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LPPL-1.1.json", - "referenceNumber": 17, - "name": "LaTeX Project Public License v1.1", - "licenseId": "LPPL-1.1", - "seeAlso": [ - "http://www.latex-project.org/lppl/lppl-1-1.txt" - ], - "isOsiApproved": false - }, - { - "reference": "https://spdx.org/licenses/ECL-2.0.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/ECL-2.0.json", - "referenceNumber": 18, - "name": "Educational Community License v2.0", - "licenseId": "ECL-2.0", + "detailsUrl": "https://spdx.org/licenses/AGPL-3.0-or-later.json", + "referenceNumber": 254, + "name": "GNU Affero General Public License v3.0 or later", + "licenseId": "AGPL-3.0-or-later", "seeAlso": [ - "https://opensource.org/licenses/ECL-2.0" + "https://www.gnu.org/licenses/agpl.txt", + "https://opensource.org/licenses/AGPL-3.0" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Entessa.html", + "reference": "https://spdx.org/licenses/Aladdin.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Entessa.json", - "referenceNumber": 19, - "name": "Entessa Public License v1.0", - "licenseId": "Entessa", + "detailsUrl": "https://spdx.org/licenses/Aladdin.json", + "referenceNumber": 294, + "name": "Aladdin Free Public License", + "licenseId": "Aladdin", "seeAlso": [ - "https://opensource.org/licenses/Entessa" + "http://pages.cs.wisc.edu/~ghost/doc/AFPL/6.01/Public.htm" ], - "isOsiApproved": true + "isOsiApproved": false, + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/Spencer-86.html", + "reference": "https://spdx.org/licenses/AMDPLPA.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Spencer-86.json", - "referenceNumber": 20, - "name": "Spencer License 86", - "licenseId": "Spencer-86", + "detailsUrl": "https://spdx.org/licenses/AMDPLPA.json", + "referenceNumber": 463, + "name": "AMD\u0027s plpa_map.c License", + "licenseId": "AMDPLPA", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Henry_Spencer_Reg-Ex_Library_License" + "https://fedoraproject.org/wiki/Licensing/AMD_plpa_map_License" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/O-UDA-1.0.html", + "reference": "https://spdx.org/licenses/AML.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/O-UDA-1.0.json", - "referenceNumber": 21, - "name": "Open Use of Data Agreement v1.0", - "licenseId": "O-UDA-1.0", + "detailsUrl": "https://spdx.org/licenses/AML.json", + "referenceNumber": 117, + "name": "Apple MIT License", + "licenseId": "AML", "seeAlso": [ - "https://github.com/microsoft/Open-Use-of-Data-Agreement/blob/v1.0/O-UDA-1.0.md", - "https://cdla.dev/open-use-of-data-agreement-v1-0/" + "https://fedoraproject.org/wiki/Licensing/Apple_MIT_License" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BSD-4-Clause.html", + "reference": "https://spdx.org/licenses/AMPAS.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-4-Clause.json", - "referenceNumber": 22, - "name": "BSD 4-Clause \"Original\" or \"Old\" License", - "licenseId": "BSD-4-Clause", + "detailsUrl": "https://spdx.org/licenses/AMPAS.json", + "referenceNumber": 223, + "name": "Academy of Motion Picture Arts and Sciences BSD", + "licenseId": "AMPAS", "seeAlso": [ - "http://directory.fsf.org/wiki/License:BSD_4Clause" + "https://fedoraproject.org/wiki/Licensing/BSD#AMPASBSD" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Condor-1.1.html", + "reference": "https://spdx.org/licenses/ANTLR-PD.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Condor-1.1.json", - "referenceNumber": 23, - "name": "Condor Public License v1.1", - "licenseId": "Condor-1.1", + "detailsUrl": "https://spdx.org/licenses/ANTLR-PD.json", + "referenceNumber": 206, + "name": "ANTLR Software Rights Notice", + "licenseId": "ANTLR-PD", "seeAlso": [ - "http://research.cs.wisc.edu/condor/license.html#condor", - "http://web.archive.org/web/20111123062036/http://research.cs.wisc.edu/condor/license.html#condor" + "http://www.antlr2.org/license.html" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/ODC-By-1.0.html", + "reference": "https://spdx.org/licenses/ANTLR-PD-fallback.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/ODC-By-1.0.json", - "referenceNumber": 24, - "name": "Open Data Commons Attribution License v1.0", - "licenseId": "ODC-By-1.0", + "detailsUrl": "https://spdx.org/licenses/ANTLR-PD-fallback.json", + "referenceNumber": 256, + "name": "ANTLR Software Rights Notice with license fallback", + "licenseId": "ANTLR-PD-fallback", "seeAlso": [ - "https://opendatacommons.org/licenses/by/1.0/" + "http://www.antlr2.org/license.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/IPA.html", + "reference": "https://spdx.org/licenses/Apache-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/IPA.json", - "referenceNumber": 25, - "name": "IPA Font License", - "licenseId": "IPA", + "detailsUrl": "https://spdx.org/licenses/Apache-1.0.json", + "referenceNumber": 491, + "name": "Apache License 1.0", + "licenseId": "Apache-1.0", "seeAlso": [ - "https://opensource.org/licenses/IPA" + "http://www.apache.org/licenses/LICENSE-1.0" ], - "isOsiApproved": true, + "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Net-SNMP.html", + "reference": "https://spdx.org/licenses/Apache-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Net-SNMP.json", - "referenceNumber": 26, - "name": "Net-SNMP License", - "licenseId": "Net-SNMP", + "detailsUrl": "https://spdx.org/licenses/Apache-1.1.json", + "referenceNumber": 153, + "name": "Apache License 1.1", + "licenseId": "Apache-1.1", "seeAlso": [ - "http://net-snmp.sourceforge.net/about/license.html" + "http://apache.org/licenses/LICENSE-1.1", + "https://opensource.org/licenses/Apache-1.1" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/BSD-4-Clause-Shortened.html", + "reference": "https://spdx.org/licenses/Apache-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-4-Clause-Shortened.json", - "referenceNumber": 27, - "name": "BSD 4 Clause Shortened", - "licenseId": "BSD-4-Clause-Shortened", + "detailsUrl": "https://spdx.org/licenses/Apache-2.0.json", + "referenceNumber": 78, + "name": "Apache License 2.0", + "licenseId": "Apache-2.0", "seeAlso": [ - "https://metadata.ftp-master.debian.org/changelogs//main/a/arpwatch/arpwatch_2.1a15-7_copyright" + "https://www.apache.org/licenses/LICENSE-2.0", + "https://opensource.org/licenses/Apache-2.0" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CDLA-Permissive-2.0.html", + "reference": "https://spdx.org/licenses/APAFML.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CDLA-Permissive-2.0.json", - "referenceNumber": 28, - "name": "Community Data License Agreement Permissive 2.0", - "licenseId": "CDLA-Permissive-2.0", + "detailsUrl": "https://spdx.org/licenses/APAFML.json", + "referenceNumber": 273, + "name": "Adobe Postscript AFM License", + "licenseId": "APAFML", "seeAlso": [ - "https://cdla.dev/permissive-2-0" + "https://fedoraproject.org/wiki/Licensing/AdobePostscriptAFM" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-ND-2.5.html", + "reference": "https://spdx.org/licenses/APL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-ND-2.5.json", - "referenceNumber": 29, - "name": "Creative Commons Attribution Non Commercial No Derivatives 2.5 Generic", - "licenseId": "CC-BY-NC-ND-2.5", + "detailsUrl": "https://spdx.org/licenses/APL-1.0.json", + "referenceNumber": 316, + "name": "Adaptive Public License 1.0", + "licenseId": "APL-1.0", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc-nd/2.5/legalcode" + "https://opensource.org/licenses/APL-1.0" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/BSD-3-Clause-No-Nuclear-License.html", + "reference": "https://spdx.org/licenses/App-s2p.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-No-Nuclear-License.json", - "referenceNumber": 30, - "name": "BSD 3-Clause No Nuclear License", - "licenseId": "BSD-3-Clause-No-Nuclear-License", + "detailsUrl": "https://spdx.org/licenses/App-s2p.json", + "referenceNumber": 448, + "name": "App::s2p License", + "licenseId": "App-s2p", "seeAlso": [ - "http://download.oracle.com/otn-pub/java/licenses/bsd.txt?AuthParam\u003d1467140197_43d516ce1776bd08a58235a7785be1cc" + "https://fedoraproject.org/wiki/Licensing/App-s2p" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-3.0-NL.html", + "reference": "https://spdx.org/licenses/APSL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-3.0-NL.json", - "referenceNumber": 31, - "name": "Creative Commons Attribution 3.0 Netherlands", - "licenseId": "CC-BY-3.0-NL", + "detailsUrl": "https://spdx.org/licenses/APSL-1.0.json", + "referenceNumber": 66, + "name": "Apple Public Source License 1.0", + "licenseId": "APSL-1.0", "seeAlso": [ - "https://creativecommons.org/licenses/by/3.0/nl/legalcode" + "https://fedoraproject.org/wiki/Licensing/Apple_Public_Source_License_1.0" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/Latex2e.html", + "reference": "https://spdx.org/licenses/APSL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Latex2e.json", - "referenceNumber": 32, - "name": "Latex2e License", - "licenseId": "Latex2e", + "detailsUrl": "https://spdx.org/licenses/APSL-1.1.json", + "referenceNumber": 277, + "name": "Apple Public Source License 1.1", + "licenseId": "APSL-1.1", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Latex2e" + "http://www.opensource.apple.com/source/IOSerialFamily/IOSerialFamily-7/APPLE_LICENSE" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/OGL-Canada-2.0.html", + "reference": "https://spdx.org/licenses/APSL-1.2.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OGL-Canada-2.0.json", - "referenceNumber": 33, - "name": "Open Government Licence - Canada", - "licenseId": "OGL-Canada-2.0", - "seeAlso": [ - "https://open.canada.ca/en/open-government-licence-canada" - ], - "isOsiApproved": false - }, - { - "reference": "https://spdx.org/licenses/GPL-2.0-with-font-exception.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GPL-2.0-with-font-exception.json", - "referenceNumber": 34, - "name": "GNU General Public License v2.0 w/Font exception", - "licenseId": "GPL-2.0-with-font-exception", + "detailsUrl": "https://spdx.org/licenses/APSL-1.2.json", + "referenceNumber": 278, + "name": "Apple Public Source License 1.2", + "licenseId": "APSL-1.2", "seeAlso": [ - "https://www.gnu.org/licenses/gpl-faq.html#FontException" + "http://www.samurajdata.se/opensource/mirror/licenses/apsl.php" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/ZPL-2.1.html", + "reference": "https://spdx.org/licenses/APSL-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/ZPL-2.1.json", - "referenceNumber": 35, - "name": "Zope Public License 2.1", - "licenseId": "ZPL-2.1", + "detailsUrl": "https://spdx.org/licenses/APSL-2.0.json", + "referenceNumber": 65, + "name": "Apple Public Source License 2.0", + "licenseId": "APSL-2.0", "seeAlso": [ - "http://old.zope.org/Resources/ZPL/" + "http://www.opensource.apple.com/license/apsl/" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/BSD-3-Clause-Modification.html", + "reference": "https://spdx.org/licenses/Arphic-1999.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-Modification.json", - "referenceNumber": 36, - "name": "BSD 3-Clause Modification", - "licenseId": "BSD-3-Clause-Modification", + "detailsUrl": "https://spdx.org/licenses/Arphic-1999.json", + "referenceNumber": 168, + "name": "Arphic Public License", + "licenseId": "Arphic-1999", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing:BSD#Modification_Variant" + "http://ftp.gnu.org/gnu/non-gnu/chinese-fonts-truetype/LICENSE" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/VSL-1.0.html", + "reference": "https://spdx.org/licenses/Artistic-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/VSL-1.0.json", - "referenceNumber": 37, - "name": "Vovida Software License v1.0", - "licenseId": "VSL-1.0", + "detailsUrl": "https://spdx.org/licenses/Artistic-1.0.json", + "referenceNumber": 70, + "name": "Artistic License 1.0", + "licenseId": "Artistic-1.0", "seeAlso": [ - "https://opensource.org/licenses/VSL-1.0" + "https://opensource.org/licenses/Artistic-1.0" ], - "isOsiApproved": true + "isOsiApproved": true, + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/RSA-MD.html", + "reference": "https://spdx.org/licenses/Artistic-1.0-cl8.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/RSA-MD.json", - "referenceNumber": 38, - "name": "RSA Message-Digest License", - "licenseId": "RSA-MD", + "detailsUrl": "https://spdx.org/licenses/Artistic-1.0-cl8.json", + "referenceNumber": 264, + "name": "Artistic License 1.0 w/clause 8", + "licenseId": "Artistic-1.0-cl8", "seeAlso": [ - "http://www.faqs.org/rfcs/rfc1321.html" + "https://opensource.org/licenses/Artistic-1.0" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/CDL-1.0.html", + "reference": "https://spdx.org/licenses/Artistic-1.0-Perl.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CDL-1.0.json", - "referenceNumber": 39, - "name": "Common Documentation License 1.0", - "licenseId": "CDL-1.0", + "detailsUrl": "https://spdx.org/licenses/Artistic-1.0-Perl.json", + "referenceNumber": 481, + "name": "Artistic License 1.0 (Perl)", + "licenseId": "Artistic-1.0-Perl", "seeAlso": [ - "http://www.opensource.apple.com/cdl/", - "https://fedoraproject.org/wiki/Licensing/Common_Documentation_License", - "https://www.gnu.org/licenses/license-list.html#ACDL" + "http://dev.perl.org/licenses/artistic.html" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/Parity-7.0.0.html", + "reference": "https://spdx.org/licenses/Artistic-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Parity-7.0.0.json", - "referenceNumber": 40, - "name": "The Parity Public License 7.0.0", - "licenseId": "Parity-7.0.0", + "detailsUrl": "https://spdx.org/licenses/Artistic-2.0.json", + "referenceNumber": 432, + "name": "Artistic License 2.0", + "licenseId": "Artistic-2.0", "seeAlso": [ - "https://paritylicense.com/versions/7.0.0.html" + "http://www.perlfoundation.org/artistic_license_2_0", + "https://www.perlfoundation.org/artistic-license-20.html", + "https://opensource.org/licenses/artistic-license-2.0" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CC-BY-SA-2.0-UK.html", + "reference": "https://spdx.org/licenses/Baekmuk.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-2.0-UK.json", - "referenceNumber": 41, - "name": "Creative Commons Attribution Share Alike 2.0 England and Wales", - "licenseId": "CC-BY-SA-2.0-UK", + "detailsUrl": "https://spdx.org/licenses/Baekmuk.json", + "referenceNumber": 28, + "name": "Baekmuk License", + "licenseId": "Baekmuk", "seeAlso": [ - "https://creativecommons.org/licenses/by-sa/2.0/uk/legalcode" + "https://fedoraproject.org/wiki/Licensing:Baekmuk?rd\u003dLicensing/Baekmuk" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/SCEA.html", + "reference": "https://spdx.org/licenses/Bahyph.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SCEA.json", - "referenceNumber": 42, - "name": "SCEA Shared Source License", - "licenseId": "SCEA", + "detailsUrl": "https://spdx.org/licenses/Bahyph.json", + "referenceNumber": 220, + "name": "Bahyph License", + "licenseId": "Bahyph", "seeAlso": [ - "http://research.scea.com/scea_shared_source_license.html" + "https://fedoraproject.org/wiki/Licensing/Bahyph" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-SA-3.0-DE.html", + "reference": "https://spdx.org/licenses/Barr.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-3.0-DE.json", - "referenceNumber": 43, - "name": "Creative Commons Attribution Non Commercial Share Alike 3.0 Germany", - "licenseId": "CC-BY-NC-SA-3.0-DE", + "detailsUrl": "https://spdx.org/licenses/Barr.json", + "referenceNumber": 442, + "name": "Barr License", + "licenseId": "Barr", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc-sa/3.0/de/legalcode" + "https://fedoraproject.org/wiki/Licensing/Barr" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/SHL-0.5.html", + "reference": "https://spdx.org/licenses/Beerware.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SHL-0.5.json", - "referenceNumber": 44, - "name": "Solderpad Hardware License v0.5", - "licenseId": "SHL-0.5", + "detailsUrl": "https://spdx.org/licenses/Beerware.json", + "referenceNumber": 126, + "name": "Beerware License", + "licenseId": "Beerware", "seeAlso": [ - "https://solderpad.org/licenses/SHL-0.5/" + "https://fedoraproject.org/wiki/Licensing/Beerware", + "https://people.freebsd.org/~phk/" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BSD-3-Clause.html", + "reference": "https://spdx.org/licenses/Bitstream-Charter.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause.json", - "referenceNumber": 45, - "name": "BSD 3-Clause \"New\" or \"Revised\" License", - "licenseId": "BSD-3-Clause", + "detailsUrl": "https://spdx.org/licenses/Bitstream-Charter.json", + "referenceNumber": 446, + "name": "Bitstream Charter Font License", + "licenseId": "Bitstream-Charter", "seeAlso": [ - "https://opensource.org/licenses/BSD-3-Clause" + "https://fedoraproject.org/wiki/Licensing/Charter#License_Text", + "https://raw.githubusercontent.com/blackhole89/notekit/master/data/fonts/Charter%20license.txt" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/NLOD-2.0.html", + "reference": "https://spdx.org/licenses/Bitstream-Vera.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NLOD-2.0.json", - "referenceNumber": 46, - "name": "Norwegian Licence for Open Government Data (NLOD) 2.0", - "licenseId": "NLOD-2.0", + "detailsUrl": "https://spdx.org/licenses/Bitstream-Vera.json", + "referenceNumber": 58, + "name": "Bitstream Vera Font License", + "licenseId": "Bitstream-Vera", "seeAlso": [ - "http://data.norge.no/nlod/en/2.0" + "https://web.archive.org/web/20080207013128/http://www.gnome.org/fonts/", + "https://docubrain.com/sites/default/files/licenses/bitstream-vera.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Saxpath.html", + "reference": "https://spdx.org/licenses/BitTorrent-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Saxpath.json", - "referenceNumber": 47, - "name": "Saxpath License", - "licenseId": "Saxpath", + "detailsUrl": "https://spdx.org/licenses/BitTorrent-1.0.json", + "referenceNumber": 203, + "name": "BitTorrent Open Source License v1.0", + "licenseId": "BitTorrent-1.0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Saxpath_License" + "http://sources.gentoo.org/cgi-bin/viewvc.cgi/gentoo-x86/licenses/BitTorrent?r1\u003d1.1\u0026r2\u003d1.1.1.1\u0026diff_format\u003ds" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/APAFML.html", + "reference": "https://spdx.org/licenses/BitTorrent-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/APAFML.json", - "referenceNumber": 48, - "name": "Adobe Postscript AFM License", - "licenseId": "APAFML", + "detailsUrl": "https://spdx.org/licenses/BitTorrent-1.1.json", + "referenceNumber": 412, + "name": "BitTorrent Open Source License v1.1", + "licenseId": "BitTorrent-1.1", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/AdobePostscriptAFM" + "http://directory.fsf.org/wiki/License:BitTorrentOSL1.1" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Info-ZIP.html", + "reference": "https://spdx.org/licenses/blessing.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Info-ZIP.json", - "referenceNumber": 49, - "name": "Info-ZIP License", - "licenseId": "Info-ZIP", + "detailsUrl": "https://spdx.org/licenses/blessing.json", + "referenceNumber": 105, + "name": "SQLite Blessing", + "licenseId": "blessing", "seeAlso": [ - "http://www.info-zip.org/license.html" + "https://www.sqlite.org/src/artifact/e33a4df7e32d742a?ln\u003d4-9", + "https://sqlite.org/src/artifact/df5091916dbb40e6" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/APSL-1.1.html", + "reference": "https://spdx.org/licenses/BlueOak-1.0.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/APSL-1.1.json", - "referenceNumber": 50, - "name": "Apple Public Source License 1.1", - "licenseId": "APSL-1.1", + "detailsUrl": "https://spdx.org/licenses/BlueOak-1.0.0.json", + "referenceNumber": 173, + "name": "Blue Oak Model License 1.0.0", + "licenseId": "BlueOak-1.0.0", "seeAlso": [ - "http://www.opensource.apple.com/source/IOSerialFamily/IOSerialFamily-7/APPLE_LICENSE" + "https://blueoakcouncil.org/license/1.0.0" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/HTMLTIDY.html", + "reference": "https://spdx.org/licenses/Borceux.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/HTMLTIDY.json", - "referenceNumber": 51, - "name": "HTML Tidy License", - "licenseId": "HTMLTIDY", + "detailsUrl": "https://spdx.org/licenses/Borceux.json", + "referenceNumber": 29, + "name": "Borceux license", + "licenseId": "Borceux", "seeAlso": [ - "https://github.com/htacg/tidy-html5/blob/next/README/LICENSE.md" + "https://fedoraproject.org/wiki/Licensing/Borceux" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/COIL-1.0.html", + "reference": "https://spdx.org/licenses/BSD-1-Clause.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/COIL-1.0.json", - "referenceNumber": 52, - "name": "Copyfree Open Innovation License", - "licenseId": "COIL-1.0", + "detailsUrl": "https://spdx.org/licenses/BSD-1-Clause.json", + "referenceNumber": 42, + "name": "BSD 1-Clause License", + "licenseId": "BSD-1-Clause", "seeAlso": [ - "https://coil.apotheon.org/plaintext/01.0.txt" + "https://svnweb.freebsd.org/base/head/include/ifaddrs.h?revision\u003d326823" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-SA-1.0.html", + "reference": "https://spdx.org/licenses/BSD-2-Clause.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-1.0.json", - "referenceNumber": 53, - "name": "Creative Commons Attribution Non Commercial Share Alike 1.0 Generic", - "licenseId": "CC-BY-NC-SA-1.0", + "detailsUrl": "https://spdx.org/licenses/BSD-2-Clause.json", + "referenceNumber": 208, + "name": "BSD 2-Clause \"Simplified\" License", + "licenseId": "BSD-2-Clause", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc-sa/1.0/legalcode" + "https://opensource.org/licenses/BSD-2-Clause" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-2.0.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-2.0.json", - "referenceNumber": 54, - "name": "Creative Commons Attribution Non Commercial 2.0 Generic", - "licenseId": "CC-BY-NC-2.0", + "reference": "https://spdx.org/licenses/BSD-2-Clause-FreeBSD.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/BSD-2-Clause-FreeBSD.json", + "referenceNumber": 129, + "name": "BSD 2-Clause FreeBSD License", + "licenseId": "BSD-2-Clause-FreeBSD", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc/2.0/legalcode" + "http://www.freebsd.org/copyright/freebsd-license.html" ], "isOsiApproved": false, - "isFsfLibre": false + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/SSH-short.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SSH-short.json", - "referenceNumber": 55, - "name": "SSH short notice", - "licenseId": "SSH-short", + "reference": "https://spdx.org/licenses/BSD-2-Clause-NetBSD.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/BSD-2-Clause-NetBSD.json", + "referenceNumber": 196, + "name": "BSD 2-Clause NetBSD License", + "licenseId": "BSD-2-Clause-NetBSD", "seeAlso": [ - "https://github.com/openssh/openssh-portable/blob/1b11ea7c58cd5c59838b5fa574cd456d6047b2d4/pathnames.h", - "http://web.mit.edu/kolya/.f/root/athena.mit.edu/sipb.mit.edu/project/openssh/OldFiles/src/openssh-2.9.9p2/ssh-add.1", - "https://joinup.ec.europa.eu/svn/lesoll/trunk/italc/lib/src/dsa_key.cpp" + "http://www.netbsd.org/about/redistribution.html#default" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Mup.html", + "reference": "https://spdx.org/licenses/BSD-2-Clause-Patent.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Mup.json", - "referenceNumber": 56, - "name": "Mup License", - "licenseId": "Mup", + "detailsUrl": "https://spdx.org/licenses/BSD-2-Clause-Patent.json", + "referenceNumber": 7, + "name": "BSD-2-Clause Plus Patent License", + "licenseId": "BSD-2-Clause-Patent", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Mup" + "https://opensource.org/licenses/BSDplusPatent" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/EFL-1.0.html", + "reference": "https://spdx.org/licenses/BSD-2-Clause-Views.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/EFL-1.0.json", - "referenceNumber": 57, - "name": "Eiffel Forum License v1.0", - "licenseId": "EFL-1.0", + "detailsUrl": "https://spdx.org/licenses/BSD-2-Clause-Views.json", + "referenceNumber": 502, + "name": "BSD 2-Clause with views sentence", + "licenseId": "BSD-2-Clause-Views", "seeAlso": [ - "http://www.eiffel-nice.org/license/forum.txt", - "https://opensource.org/licenses/EFL-1.0" + "http://www.freebsd.org/copyright/freebsd-license.html", + "https://people.freebsd.org/~ivoras/wine/patch-wine-nvidia.sh", + "https://github.com/protegeproject/protege/blob/master/license.txt" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/LGPL-3.0-only.html", + "reference": "https://spdx.org/licenses/BSD-3-Clause.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LGPL-3.0-only.json", - "referenceNumber": 58, - "name": "GNU Lesser General Public License v3.0 only", - "licenseId": "LGPL-3.0-only", + "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause.json", + "referenceNumber": 39, + "name": "BSD 3-Clause \"New\" or \"Revised\" License", + "licenseId": "BSD-3-Clause", "seeAlso": [ - "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", - "https://opensource.org/licenses/LGPL-3.0" + "https://opensource.org/licenses/BSD-3-Clause", + "https://www.eclipse.org/org/documents/edl-v10.php" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/NPOSL-3.0.html", + "reference": "https://spdx.org/licenses/BSD-3-Clause-Attribution.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NPOSL-3.0.json", - "referenceNumber": 59, - "name": "Non-Profit Open Software License 3.0", - "licenseId": "NPOSL-3.0", + "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-Attribution.json", + "referenceNumber": 353, + "name": "BSD with attribution", + "licenseId": "BSD-3-Clause-Attribution", "seeAlso": [ - "https://opensource.org/licenses/NOSL3.0" + "https://fedoraproject.org/wiki/Licensing/BSD_with_Attribution" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-ND-4.0.html", + "reference": "https://spdx.org/licenses/BSD-3-Clause-Clear.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-ND-4.0.json", - "referenceNumber": 60, - "name": "Creative Commons Attribution No Derivatives 4.0 International", - "licenseId": "CC-BY-ND-4.0", + "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-Clear.json", + "referenceNumber": 397, + "name": "BSD 3-Clause Clear License", + "licenseId": "BSD-3-Clause-Clear", "seeAlso": [ - "https://creativecommons.org/licenses/by-nd/4.0/legalcode" + "http://labs.metacarta.com/license-explanation.html#license" ], "isOsiApproved": false, - "isFsfLibre": false + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Xerox.html", + "reference": "https://spdx.org/licenses/BSD-3-Clause-LBNL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Xerox.json", - "referenceNumber": 61, - "name": "Xerox License", - "licenseId": "Xerox", + "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-LBNL.json", + "referenceNumber": 301, + "name": "Lawrence Berkeley National Labs BSD variant license", + "licenseId": "BSD-3-Clause-LBNL", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Xerox" + "https://fedoraproject.org/wiki/Licensing/LBNLBSD" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/OGL-UK-3.0.html", + "reference": "https://spdx.org/licenses/BSD-3-Clause-Modification.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OGL-UK-3.0.json", - "referenceNumber": 62, - "name": "Open Government Licence v3.0", - "licenseId": "OGL-UK-3.0", + "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-Modification.json", + "referenceNumber": 214, + "name": "BSD 3-Clause Modification", + "licenseId": "BSD-3-Clause-Modification", "seeAlso": [ - "http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" + "https://fedoraproject.org/wiki/Licensing:BSD#Modification_Variant" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/AAL.html", + "reference": "https://spdx.org/licenses/BSD-3-Clause-No-Military-License.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/AAL.json", - "referenceNumber": 63, - "name": "Attribution Assurance License", - "licenseId": "AAL", + "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-No-Military-License.json", + "referenceNumber": 23, + "name": "BSD 3-Clause No Military License", + "licenseId": "BSD-3-Clause-No-Military-License", "seeAlso": [ - "https://opensource.org/licenses/attribution" + "https://gitlab.syncad.com/hive/dhive/-/blob/master/LICENSE", + "https://github.com/greymass/swift-eosio/blob/master/LICENSE" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/QPL-1.0.html", + "reference": "https://spdx.org/licenses/BSD-3-Clause-No-Nuclear-License.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/QPL-1.0.json", - "referenceNumber": 64, - "name": "Q Public License 1.0", - "licenseId": "QPL-1.0", + "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-No-Nuclear-License.json", + "referenceNumber": 393, + "name": "BSD 3-Clause No Nuclear License", + "licenseId": "BSD-3-Clause-No-Nuclear-License", "seeAlso": [ - "http://doc.qt.nokia.com/3.3/license.html", - "https://opensource.org/licenses/QPL-1.0", - "https://doc.qt.io/archives/3.3/license.html" + "http://download.oracle.com/otn-pub/java/licenses/bsd.txt?AuthParam\u003d1467140197_43d516ce1776bd08a58235a7785be1cc" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Dotseqn.html", + "reference": "https://spdx.org/licenses/BSD-3-Clause-No-Nuclear-License-2014.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Dotseqn.json", - "referenceNumber": 65, - "name": "Dotseqn License", - "licenseId": "Dotseqn", + "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-No-Nuclear-License-2014.json", + "referenceNumber": 356, + "name": "BSD 3-Clause No Nuclear License 2014", + "licenseId": "BSD-3-Clause-No-Nuclear-License-2014", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Dotseqn" + "https://java.net/projects/javaeetutorial/pages/BerkeleyLicense" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GPL-2.0.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GPL-2.0.json", - "referenceNumber": 66, - "name": "GNU General Public License v2.0 only", - "licenseId": "GPL-2.0", - "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", - "https://opensource.org/licenses/GPL-2.0" - ], - "isOsiApproved": true, - "isFsfLibre": true - }, - { - "reference": "https://spdx.org/licenses/libselinux-1.0.html", + "reference": "https://spdx.org/licenses/BSD-3-Clause-No-Nuclear-Warranty.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/libselinux-1.0.json", - "referenceNumber": 67, - "name": "libselinux public domain notice", - "licenseId": "libselinux-1.0", + "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-No-Nuclear-Warranty.json", + "referenceNumber": 225, + "name": "BSD 3-Clause No Nuclear Warranty", + "licenseId": "BSD-3-Clause-No-Nuclear-Warranty", "seeAlso": [ - "https://github.com/SELinuxProject/selinux/blob/master/libselinux/LICENSE" + "https://jogamp.org/git/?p\u003dgluegen.git;a\u003dblob_plain;f\u003dLICENSE.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/MIT-advertising.html", + "reference": "https://spdx.org/licenses/BSD-3-Clause-Open-MPI.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MIT-advertising.json", - "referenceNumber": 68, - "name": "Enlightenment License (e16)", - "licenseId": "MIT-advertising", + "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-Open-MPI.json", + "referenceNumber": 389, + "name": "BSD 3-Clause Open MPI variant", + "licenseId": "BSD-3-Clause-Open-MPI", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/MIT_With_Advertising" + "https://www.open-mpi.org/community/license.php", + "http://www.netlib.org/lapack/LICENSE.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/SSH-OpenSSH.html", + "reference": "https://spdx.org/licenses/BSD-4-Clause.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SSH-OpenSSH.json", - "referenceNumber": 69, - "name": "SSH OpenSSH license", - "licenseId": "SSH-OpenSSH", + "detailsUrl": "https://spdx.org/licenses/BSD-4-Clause.json", + "referenceNumber": 507, + "name": "BSD 4-Clause \"Original\" or \"Old\" License", + "licenseId": "BSD-4-Clause", "seeAlso": [ - "https://github.com/openssh/openssh-portable/blob/1b11ea7c58cd5c59838b5fa574cd456d6047b2d4/LICENCE#L10" + "http://directory.fsf.org/wiki/License:BSD_4Clause" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/LGPLLR.html", + "reference": "https://spdx.org/licenses/BSD-4-Clause-Shortened.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LGPLLR.json", - "referenceNumber": 70, - "name": "Lesser General Public License For Linguistic Resources", - "licenseId": "LGPLLR", + "detailsUrl": "https://spdx.org/licenses/BSD-4-Clause-Shortened.json", + "referenceNumber": 8, + "name": "BSD 4 Clause Shortened", + "licenseId": "BSD-4-Clause-Shortened", "seeAlso": [ - "http://www-igm.univ-mlv.fr/~unitex/lgpllr.html" + "https://metadata.ftp-master.debian.org/changelogs//main/a/arpwatch/arpwatch_2.1a15-7_copyright" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/ErlPL-1.1.html", + "reference": "https://spdx.org/licenses/BSD-4-Clause-UC.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/ErlPL-1.1.json", - "referenceNumber": 71, - "name": "Erlang Public License v1.1", - "licenseId": "ErlPL-1.1", + "detailsUrl": "https://spdx.org/licenses/BSD-4-Clause-UC.json", + "referenceNumber": 259, + "name": "BSD-4-Clause (University of California-Specific)", + "licenseId": "BSD-4-Clause-UC", "seeAlso": [ - "http://www.erlang.org/EPLICENSE" + "http://www.freebsd.org/copyright/license.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BSD-2-Clause-NetBSD.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/BSD-2-Clause-NetBSD.json", - "referenceNumber": 72, - "name": "BSD 2-Clause NetBSD License", - "licenseId": "BSD-2-Clause-NetBSD", + "reference": "https://spdx.org/licenses/BSD-Protection.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/BSD-Protection.json", + "referenceNumber": 241, + "name": "BSD Protection License", + "licenseId": "BSD-Protection", "seeAlso": [ - "http://www.netbsd.org/about/redistribution.html#default" + "https://fedoraproject.org/wiki/Licensing/BSD_Protection_License" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/TU-Berlin-2.0.html", + "reference": "https://spdx.org/licenses/BSD-Source-Code.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/TU-Berlin-2.0.json", - "referenceNumber": 73, - "name": "Technische Universitaet Berlin License 2.0", - "licenseId": "TU-Berlin-2.0", + "detailsUrl": "https://spdx.org/licenses/BSD-Source-Code.json", + "referenceNumber": 101, + "name": "BSD Source Code Attribution", + "licenseId": "BSD-Source-Code", "seeAlso": [ - "https://github.com/CorsixTH/deps/blob/fd339a9f526d1d9c9f01ccf39e438a015da50035/licences/libgsm.txt" + "https://github.com/robbiehanson/CocoaHTTPServer/blob/master/LICENSE.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-ND-2.0.html", + "reference": "https://spdx.org/licenses/BSL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-ND-2.0.json", - "referenceNumber": 74, - "name": "Creative Commons Attribution No Derivatives 2.0 Generic", - "licenseId": "CC-BY-ND-2.0", + "detailsUrl": "https://spdx.org/licenses/BSL-1.0.json", + "referenceNumber": 140, + "name": "Boost Software License 1.0", + "licenseId": "BSL-1.0", "seeAlso": [ - "https://creativecommons.org/licenses/by-nd/2.0/legalcode" + "http://www.boost.org/LICENSE_1_0.txt", + "https://opensource.org/licenses/BSL-1.0" ], - "isOsiApproved": false, - "isFsfLibre": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-ND-4.0.html", + "reference": "https://spdx.org/licenses/BUSL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-ND-4.0.json", - "referenceNumber": 75, - "name": "Creative Commons Attribution Non Commercial No Derivatives 4.0 International", - "licenseId": "CC-BY-NC-ND-4.0", + "detailsUrl": "https://spdx.org/licenses/BUSL-1.1.json", + "referenceNumber": 250, + "name": "Business Source License 1.1", + "licenseId": "BUSL-1.1", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode" + "https://mariadb.com/bsl11/" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Plexus.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Plexus.json", - "referenceNumber": 76, - "name": "Plexus Classworlds License", - "licenseId": "Plexus", + "reference": "https://spdx.org/licenses/bzip2-1.0.5.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/bzip2-1.0.5.json", + "referenceNumber": 30, + "name": "bzip2 and libbzip2 License v1.0.5", + "licenseId": "bzip2-1.0.5", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Plexus_Classworlds_License" + "https://sourceware.org/bzip2/1.0.5/bzip2-manual-1.0.5.html", + "http://bzip.org/1.0.5/bzip2-manual-1.0.5.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/SPL-1.0.html", + "reference": "https://spdx.org/licenses/bzip2-1.0.6.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SPL-1.0.json", - "referenceNumber": 77, - "name": "Sun Public License v1.0", - "licenseId": "SPL-1.0", + "detailsUrl": "https://spdx.org/licenses/bzip2-1.0.6.json", + "referenceNumber": 306, + "name": "bzip2 and libbzip2 License v1.0.6", + "licenseId": "bzip2-1.0.6", "seeAlso": [ - "https://opensource.org/licenses/SPL-1.0" + "https://sourceware.org/git/?p\u003dbzip2.git;a\u003dblob;f\u003dLICENSE;hb\u003dbzip2-1.0.6", + "http://bzip.org/1.0.5/bzip2-manual-1.0.5.html" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BSD-3-Clause-No-Military-License.html", + "reference": "https://spdx.org/licenses/C-UDA-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-No-Military-License.json", - "referenceNumber": 78, - "name": "BSD 3-Clause No Military License", - "licenseId": "BSD-3-Clause-No-Military-License", + "detailsUrl": "https://spdx.org/licenses/C-UDA-1.0.json", + "referenceNumber": 279, + "name": "Computational Use of Data Agreement v1.0", + "licenseId": "C-UDA-1.0", "seeAlso": [ - "https://gitlab.syncad.com/hive/dhive/-/blob/master/LICENSE", - "https://github.com/greymass/swift-eosio/blob/master/LICENSE" + "https://github.com/microsoft/Computational-Use-of-Data-Agreement/blob/master/C-UDA-1.0.md", + "https://cdla.dev/computational-use-of-data-agreement-v1-0/" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CPAL-1.0.html", + "reference": "https://spdx.org/licenses/CAL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CPAL-1.0.json", - "referenceNumber": 79, - "name": "Common Public Attribution License 1.0", - "licenseId": "CPAL-1.0", + "detailsUrl": "https://spdx.org/licenses/CAL-1.0.json", + "referenceNumber": 504, + "name": "Cryptographic Autonomy License 1.0", + "licenseId": "CAL-1.0", "seeAlso": [ - "https://opensource.org/licenses/CPAL-1.0" + "http://cryptographicautonomylicense.com/license-text.html", + "https://opensource.org/licenses/CAL-1.0" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/Apache-1.1.html", + "reference": "https://spdx.org/licenses/CAL-1.0-Combined-Work-Exception.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Apache-1.1.json", - "referenceNumber": 80, - "name": "Apache License 1.1", - "licenseId": "Apache-1.1", + "detailsUrl": "https://spdx.org/licenses/CAL-1.0-Combined-Work-Exception.json", + "referenceNumber": 329, + "name": "Cryptographic Autonomy License 1.0 (Combined Work Exception)", + "licenseId": "CAL-1.0-Combined-Work-Exception", "seeAlso": [ - "http://apache.org/licenses/LICENSE-1.1", - "https://opensource.org/licenses/Apache-1.1" + "http://cryptographicautonomylicense.com/license-text.html", + "https://opensource.org/licenses/CAL-1.0" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/UPL-1.0.html", + "reference": "https://spdx.org/licenses/Caldera.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/UPL-1.0.json", - "referenceNumber": 81, - "name": "Universal Permissive License v1.0", - "licenseId": "UPL-1.0", + "detailsUrl": "https://spdx.org/licenses/Caldera.json", + "referenceNumber": 333, + "name": "Caldera License", + "licenseId": "Caldera", "seeAlso": [ - "https://opensource.org/licenses/UPL" + "http://www.lemis.com/grog/UNIX/ancient-source-all.pdf" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GFDL-1.1-no-invariants-or-later.html", + "reference": "https://spdx.org/licenses/CATOSL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.1-no-invariants-or-later.json", - "referenceNumber": 82, - "name": "GNU Free Documentation License v1.1 or later - no invariants", - "licenseId": "GFDL-1.1-no-invariants-or-later", + "detailsUrl": "https://spdx.org/licenses/CATOSL-1.1.json", + "referenceNumber": 451, + "name": "Computer Associates Trusted Open Source License 1.1", + "licenseId": "CATOSL-1.1", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" + "https://opensource.org/licenses/CATOSL-1.1" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/GFDL-1.2-invariants-or-later.html", + "reference": "https://spdx.org/licenses/CC-BY-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.2-invariants-or-later.json", - "referenceNumber": 83, - "name": "GNU Free Documentation License v1.2 or later - invariants", - "licenseId": "GFDL-1.2-invariants-or-later", + "detailsUrl": "https://spdx.org/licenses/CC-BY-1.0.json", + "referenceNumber": 163, + "name": "Creative Commons Attribution 1.0 Generic", + "licenseId": "CC-BY-1.0", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" + "https://creativecommons.org/licenses/by/1.0/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/mpich2.html", + "reference": "https://spdx.org/licenses/CC-BY-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/mpich2.json", - "referenceNumber": 84, - "name": "mpich2 License", - "licenseId": "mpich2", + "detailsUrl": "https://spdx.org/licenses/CC-BY-2.0.json", + "referenceNumber": 437, + "name": "Creative Commons Attribution 2.0 Generic", + "licenseId": "CC-BY-2.0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/MIT" + "https://creativecommons.org/licenses/by/2.0/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/LGPL-2.0-only.html", + "reference": "https://spdx.org/licenses/CC-BY-2.5.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LGPL-2.0-only.json", - "referenceNumber": 85, - "name": "GNU Library General Public License v2 only", - "licenseId": "LGPL-2.0-only", + "detailsUrl": "https://spdx.org/licenses/CC-BY-2.5.json", + "referenceNumber": 55, + "name": "Creative Commons Attribution 2.5 Generic", + "licenseId": "CC-BY-2.5", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/lgpl-2.0-standalone.html" + "https://creativecommons.org/licenses/by/2.5/legalcode" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/eGenix.html", + "reference": "https://spdx.org/licenses/CC-BY-2.5-AU.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/eGenix.json", - "referenceNumber": 86, - "name": "eGenix.com Public License 1.1.0", - "licenseId": "eGenix", + "detailsUrl": "https://spdx.org/licenses/CC-BY-2.5-AU.json", + "referenceNumber": 34, + "name": "Creative Commons Attribution 2.5 Australia", + "licenseId": "CC-BY-2.5-AU", "seeAlso": [ - "http://www.egenix.com/products/eGenix.com-Public-License-1.1.0.pdf", - "https://fedoraproject.org/wiki/Licensing/eGenix.com_Public_License_1.1.0" + "https://creativecommons.org/licenses/by/2.5/au/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GFDL-1.2-only.html", + "reference": "https://spdx.org/licenses/CC-BY-3.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.2-only.json", - "referenceNumber": 87, - "name": "GNU Free Documentation License v1.2 only", - "licenseId": "GFDL-1.2-only", + "detailsUrl": "https://spdx.org/licenses/CC-BY-3.0.json", + "referenceNumber": 490, + "name": "Creative Commons Attribution 3.0 Unported", + "licenseId": "CC-BY-3.0", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" + "https://creativecommons.org/licenses/by/3.0/legalcode" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Unicode-TOU.html", + "reference": "https://spdx.org/licenses/CC-BY-3.0-AT.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Unicode-TOU.json", - "referenceNumber": 88, - "name": "Unicode Terms of Use", - "licenseId": "Unicode-TOU", + "detailsUrl": "https://spdx.org/licenses/CC-BY-3.0-AT.json", + "referenceNumber": 80, + "name": "Creative Commons Attribution 3.0 Austria", + "licenseId": "CC-BY-3.0-AT", "seeAlso": [ - "http://www.unicode.org/copyright.html" + "https://creativecommons.org/licenses/by/3.0/at/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/APSL-1.2.html", + "reference": "https://spdx.org/licenses/CC-BY-3.0-DE.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/APSL-1.2.json", - "referenceNumber": 89, - "name": "Apple Public Source License 1.2", - "licenseId": "APSL-1.2", + "detailsUrl": "https://spdx.org/licenses/CC-BY-3.0-DE.json", + "referenceNumber": 166, + "name": "Creative Commons Attribution 3.0 Germany", + "licenseId": "CC-BY-3.0-DE", "seeAlso": [ - "http://www.samurajdata.se/opensource/mirror/licenses/apsl.php" + "https://creativecommons.org/licenses/by/3.0/de/legalcode" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Apache-2.0.html", + "reference": "https://spdx.org/licenses/CC-BY-3.0-IGO.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Apache-2.0.json", - "referenceNumber": 90, - "name": "Apache License 2.0", - "licenseId": "Apache-2.0", + "detailsUrl": "https://spdx.org/licenses/CC-BY-3.0-IGO.json", + "referenceNumber": 419, + "name": "Creative Commons Attribution 3.0 IGO", + "licenseId": "CC-BY-3.0-IGO", "seeAlso": [ - "https://www.apache.org/licenses/LICENSE-2.0", - "https://opensource.org/licenses/Apache-2.0" + "https://creativecommons.org/licenses/by/3.0/igo/legalcode" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BSD-Source-Code.html", + "reference": "https://spdx.org/licenses/CC-BY-3.0-NL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-Source-Code.json", - "referenceNumber": 91, - "name": "BSD Source Code Attribution", - "licenseId": "BSD-Source-Code", + "detailsUrl": "https://spdx.org/licenses/CC-BY-3.0-NL.json", + "referenceNumber": 455, + "name": "Creative Commons Attribution 3.0 Netherlands", + "licenseId": "CC-BY-3.0-NL", "seeAlso": [ - "https://github.com/robbiehanson/CocoaHTTPServer/blob/master/LICENSE.txt" + "https://creativecommons.org/licenses/by/3.0/nl/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-4.0.html", + "reference": "https://spdx.org/licenses/CC-BY-3.0-US.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-4.0.json", - "referenceNumber": 92, - "name": "Creative Commons Attribution Non Commercial 4.0 International", - "licenseId": "CC-BY-NC-4.0", + "detailsUrl": "https://spdx.org/licenses/CC-BY-3.0-US.json", + "referenceNumber": 492, + "name": "Creative Commons Attribution 3.0 United States", + "licenseId": "CC-BY-3.0-US", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc/4.0/legalcode" + "https://creativecommons.org/licenses/by/3.0/us/legalcode" ], - "isOsiApproved": false, - "isFsfLibre": false + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/TMate.html", + "reference": "https://spdx.org/licenses/CC-BY-4.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/TMate.json", - "referenceNumber": 93, - "name": "TMate Open Source License", - "licenseId": "TMate", + "detailsUrl": "https://spdx.org/licenses/CC-BY-4.0.json", + "referenceNumber": 433, + "name": "Creative Commons Attribution 4.0 International", + "licenseId": "CC-BY-4.0", "seeAlso": [ - "http://svnkit.com/license.html" + "https://creativecommons.org/licenses/by/4.0/legalcode" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/OPL-1.0.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OPL-1.0.json", - "referenceNumber": 94, - "name": "Open Public License v1.0", - "licenseId": "OPL-1.0", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-1.0.json", + "referenceNumber": 212, + "name": "Creative Commons Attribution Non Commercial 1.0 Generic", + "licenseId": "CC-BY-NC-1.0", "seeAlso": [ - "http://old.koalateam.com/jackaroo/OPL_1_0.TXT", - "https://fedoraproject.org/wiki/Licensing/Open_Public_License" + "https://creativecommons.org/licenses/by-nc/1.0/legalcode" ], "isOsiApproved": false, "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/CECILL-1.0.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CECILL-1.0.json", - "referenceNumber": 95, - "name": "CeCILL Free Software License Agreement v1.0", - "licenseId": "CECILL-1.0", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-2.0.json", + "referenceNumber": 291, + "name": "Creative Commons Attribution Non Commercial 2.0 Generic", + "licenseId": "CC-BY-NC-2.0", "seeAlso": [ - "http://www.cecill.info/licences/Licence_CeCILL_V1-fr.html" + "https://creativecommons.org/licenses/by-nc/2.0/legalcode" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/EPL-2.0.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-2.5.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/EPL-2.0.json", - "referenceNumber": 96, - "name": "Eclipse Public License 2.0", - "licenseId": "EPL-2.0", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-2.5.json", + "referenceNumber": 334, + "name": "Creative Commons Attribution Non Commercial 2.5 Generic", + "licenseId": "CC-BY-NC-2.5", "seeAlso": [ - "https://www.eclipse.org/legal/epl-2.0", - "https://www.opensource.org/licenses/EPL-2.0" + "https://creativecommons.org/licenses/by-nc/2.5/legalcode" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false, + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/LPPL-1.3c.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-3.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LPPL-1.3c.json", - "referenceNumber": 97, - "name": "LaTeX Project Public License v1.3c", - "licenseId": "LPPL-1.3c", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-3.0.json", + "referenceNumber": 350, + "name": "Creative Commons Attribution Non Commercial 3.0 Unported", + "licenseId": "CC-BY-NC-3.0", "seeAlso": [ - "http://www.latex-project.org/lppl/lppl-1-3c.txt", - "https://opensource.org/licenses/LPPL-1.3c" + "https://creativecommons.org/licenses/by-nc/3.0/legalcode" ], - "isOsiApproved": true + "isOsiApproved": false, + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/Zed.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-3.0-DE.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Zed.json", - "referenceNumber": 98, - "name": "Zed License", - "licenseId": "Zed", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-3.0-DE.json", + "referenceNumber": 475, + "name": "Creative Commons Attribution Non Commercial 3.0 Germany", + "licenseId": "CC-BY-NC-3.0-DE", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Zed" + "https://creativecommons.org/licenses/by-nc/3.0/de/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/SGI-B-2.0.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-4.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SGI-B-2.0.json", - "referenceNumber": 99, - "name": "SGI Free Software License B v2.0", - "licenseId": "SGI-B-2.0", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-4.0.json", + "referenceNumber": 90, + "name": "Creative Commons Attribution Non Commercial 4.0 International", + "licenseId": "CC-BY-NC-4.0", "seeAlso": [ - "http://oss.sgi.com/projects/FreeB/SGIFreeSWLicB.2.0.pdf" + "https://creativecommons.org/licenses/by-nc/4.0/legalcode" ], "isOsiApproved": false, - "isFsfLibre": true + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/BSD-3-Clause-Open-MPI.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-ND-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-Open-MPI.json", - "referenceNumber": 100, - "name": "BSD 3-Clause Open MPI variant", - "licenseId": "BSD-3-Clause-Open-MPI", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-ND-1.0.json", + "referenceNumber": 5, + "name": "Creative Commons Attribution Non Commercial No Derivatives 1.0 Generic", + "licenseId": "CC-BY-NC-ND-1.0", "seeAlso": [ - "https://www.open-mpi.org/community/license.php", - "http://www.netlib.org/lapack/LICENSE.txt" + "https://creativecommons.org/licenses/by-nd-nc/1.0/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CDDL-1.0.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-ND-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CDDL-1.0.json", - "referenceNumber": 101, - "name": "Common Development and Distribution License 1.0", - "licenseId": "CDDL-1.0", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-ND-2.0.json", + "referenceNumber": 274, + "name": "Creative Commons Attribution Non Commercial No Derivatives 2.0 Generic", + "licenseId": "CC-BY-NC-ND-2.0", "seeAlso": [ - "https://opensource.org/licenses/cddl1" + "https://creativecommons.org/licenses/by-nc-nd/2.0/legalcode" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GFDL-1.2-or-later.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-ND-2.5.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.2-or-later.json", - "referenceNumber": 102, - "name": "GNU Free Documentation License v1.2 or later", - "licenseId": "GFDL-1.2-or-later", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-ND-2.5.json", + "referenceNumber": 9, + "name": "Creative Commons Attribution Non Commercial No Derivatives 2.5 Generic", + "licenseId": "CC-BY-NC-ND-2.5", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" + "https://creativecommons.org/licenses/by-nc-nd/2.5/legalcode" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Hippocratic-2.1.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-ND-3.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Hippocratic-2.1.json", - "referenceNumber": 103, - "name": "Hippocratic License 2.1", - "licenseId": "Hippocratic-2.1", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-ND-3.0.json", + "referenceNumber": 497, + "name": "Creative Commons Attribution Non Commercial No Derivatives 3.0 Unported", + "licenseId": "CC-BY-NC-ND-3.0", "seeAlso": [ - "https://firstdonoharm.dev/version/2/1/license.html", - "https://github.com/EthicalSource/hippocratic-license/blob/58c0e646d64ff6fbee275bfe2b9492f914e3ab2a/LICENSE.txt" + "https://creativecommons.org/licenses/by-nc-nd/3.0/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Linux-OpenIB.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-ND-3.0-DE.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Linux-OpenIB.json", - "referenceNumber": 104, - "name": "Linux Kernel Variant of OpenIB.org license", - "licenseId": "Linux-OpenIB", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-ND-3.0-DE.json", + "referenceNumber": 399, + "name": "Creative Commons Attribution Non Commercial No Derivatives 3.0 Germany", + "licenseId": "CC-BY-NC-ND-3.0-DE", "seeAlso": [ - "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/infiniband/core/sa.h" + "https://creativecommons.org/licenses/by-nc-nd/3.0/de/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/IJG.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-ND-3.0-IGO.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/IJG.json", - "referenceNumber": 105, - "name": "Independent JPEG Group License", - "licenseId": "IJG", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-ND-3.0-IGO.json", + "referenceNumber": 370, + "name": "Creative Commons Attribution Non Commercial No Derivatives 3.0 IGO", + "licenseId": "CC-BY-NC-ND-3.0-IGO", "seeAlso": [ - "http://dev.w3.org/cvsweb/Amaya/libjpeg/Attic/README?rev\u003d1.2" + "https://creativecommons.org/licenses/by-nc-nd/3.0/igo/legalcode" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/SchemeReport.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-ND-4.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SchemeReport.json", - "referenceNumber": 106, - "name": "Scheme Language Report License", - "licenseId": "SchemeReport", - "seeAlso": [], + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-ND-4.0.json", + "referenceNumber": 310, + "name": "Creative Commons Attribution Non Commercial No Derivatives 4.0 International", + "licenseId": "CC-BY-NC-ND-4.0", + "seeAlso": [ + "https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode" + ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/TCL.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-SA-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/TCL.json", - "referenceNumber": 107, - "name": "TCL/TK License", - "licenseId": "TCL", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-1.0.json", + "referenceNumber": 369, + "name": "Creative Commons Attribution Non Commercial Share Alike 1.0 Generic", + "licenseId": "CC-BY-NC-SA-1.0", "seeAlso": [ - "http://www.tcl.tk/software/tcltk/license.html", - "https://fedoraproject.org/wiki/Licensing/TCL" + "https://creativecommons.org/licenses/by-nc-sa/1.0/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Linux-man-pages-copyleft.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-SA-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Linux-man-pages-copyleft.json", - "referenceNumber": 108, - "name": "Linux man-pages Copyleft", - "licenseId": "Linux-man-pages-copyleft", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-2.0.json", + "referenceNumber": 180, + "name": "Creative Commons Attribution Non Commercial Share Alike 2.0 Generic", + "licenseId": "CC-BY-NC-SA-2.0", "seeAlso": [ - "https://www.kernel.org/doc/man-pages/licenses.html" + "https://creativecommons.org/licenses/by-nc-sa/2.0/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/NBPL-1.0.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-SA-2.0-DE.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NBPL-1.0.json", - "referenceNumber": 109, - "name": "Net Boolean Public License v1", - "licenseId": "NBPL-1.0", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-2.0-DE.json", + "referenceNumber": 132, + "name": "Creative Commons Attribution Non Commercial Share Alike 2.0 Germany", + "licenseId": "CC-BY-NC-SA-2.0-DE", "seeAlso": [ - "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d37b4b3f6cc4bf34e1d3dec61e69914b9819d8894" + "https://creativecommons.org/licenses/by-nc-sa/2.0/de/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Zimbra-1.3.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-SA-2.0-FR.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Zimbra-1.3.json", - "referenceNumber": 110, - "name": "Zimbra Public License v1.3", - "licenseId": "Zimbra-1.3", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-2.0-FR.json", + "referenceNumber": 380, + "name": "Creative Commons Attribution-NonCommercial-ShareAlike 2.0 France", + "licenseId": "CC-BY-NC-SA-2.0-FR", "seeAlso": [ - "http://web.archive.org/web/20100302225219/http://www.zimbra.com/license/zimbra-public-license-1-3.html" + "https://creativecommons.org/licenses/by-nc-sa/2.0/fr/legalcode" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/MPL-1.0.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-SA-2.0-UK.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MPL-1.0.json", - "referenceNumber": 111, - "name": "Mozilla Public License 1.0", - "licenseId": "MPL-1.0", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-2.0-UK.json", + "referenceNumber": 296, + "name": "Creative Commons Attribution Non Commercial Share Alike 2.0 England and Wales", + "licenseId": "CC-BY-NC-SA-2.0-UK", "seeAlso": [ - "http://www.mozilla.org/MPL/MPL-1.0.html", - "https://opensource.org/licenses/MPL-1.0" + "https://creativecommons.org/licenses/by-nc-sa/2.0/uk/legalcode" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/LGPL-2.1-or-later.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-SA-2.5.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LGPL-2.1-or-later.json", - "referenceNumber": 112, - "name": "GNU Lesser General Public License v2.1 or later", - "licenseId": "LGPL-2.1-or-later", - "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", - "https://opensource.org/licenses/LGPL-2.1" - ], - "isOsiApproved": true, - "isFsfLibre": true - }, - { - "reference": "https://spdx.org/licenses/CC-PDDC.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-PDDC.json", - "referenceNumber": 113, - "name": "Creative Commons Public Domain Dedication and Certification", - "licenseId": "CC-PDDC", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-2.5.json", + "referenceNumber": 33, + "name": "Creative Commons Attribution Non Commercial Share Alike 2.5 Generic", + "licenseId": "CC-BY-NC-SA-2.5", "seeAlso": [ - "https://creativecommons.org/licenses/publicdomain/" + "https://creativecommons.org/licenses/by-nc-sa/2.5/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Imlib2.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-SA-3.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Imlib2.json", - "referenceNumber": 114, - "name": "Imlib2 License", - "licenseId": "Imlib2", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-3.0.json", + "referenceNumber": 337, + "name": "Creative Commons Attribution Non Commercial Share Alike 3.0 Unported", + "licenseId": "CC-BY-NC-SA-3.0", "seeAlso": [ - "http://trac.enlightenment.org/e/browser/trunk/imlib2/COPYING", - "https://git.enlightenment.org/legacy/imlib2.git/tree/COPYING" + "https://creativecommons.org/licenses/by-nc-sa/3.0/legalcode" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Multics.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-SA-3.0-DE.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Multics.json", - "referenceNumber": 115, - "name": "Multics License", - "licenseId": "Multics", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-3.0-DE.json", + "referenceNumber": 269, + "name": "Creative Commons Attribution Non Commercial Share Alike 3.0 Germany", + "licenseId": "CC-BY-NC-SA-3.0-DE", "seeAlso": [ - "https://opensource.org/licenses/Multics" + "https://creativecommons.org/licenses/by-nc-sa/3.0/de/legalcode" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/D-FSL-1.0.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-SA-3.0-IGO.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/D-FSL-1.0.json", - "referenceNumber": 116, - "name": "Deutsche Freie Software Lizenz", - "licenseId": "D-FSL-1.0", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-3.0-IGO.json", + "referenceNumber": 271, + "name": "Creative Commons Attribution Non Commercial Share Alike 3.0 IGO", + "licenseId": "CC-BY-NC-SA-3.0-IGO", "seeAlso": [ - "http://www.dipp.nrw.de/d-fsl/lizenzen/", - "http://www.dipp.nrw.de/d-fsl/index_html/lizenzen/de/D-FSL-1_0_de.txt", - "http://www.dipp.nrw.de/d-fsl/index_html/lizenzen/en/D-FSL-1_0_en.txt", - "https://www.hbz-nrw.de/produkte/open-access/lizenzen/dfsl", - "https://www.hbz-nrw.de/produkte/open-access/lizenzen/dfsl/deutsche-freie-software-lizenz", - "https://www.hbz-nrw.de/produkte/open-access/lizenzen/dfsl/german-free-software-license", - "https://www.hbz-nrw.de/produkte/open-access/lizenzen/dfsl/D-FSL-1_0_de.txt/at_download/file", - "https://www.hbz-nrw.de/produkte/open-access/lizenzen/dfsl/D-FSL-1_0_en.txt/at_download/file" + "https://creativecommons.org/licenses/by-nc-sa/3.0/igo/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BSD-3-Clause-No-Nuclear-Warranty.html", + "reference": "https://spdx.org/licenses/CC-BY-NC-SA-4.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-No-Nuclear-Warranty.json", - "referenceNumber": 117, - "name": "BSD 3-Clause No Nuclear Warranty", - "licenseId": "BSD-3-Clause-No-Nuclear-Warranty", + "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-4.0.json", + "referenceNumber": 434, + "name": "Creative Commons Attribution Non Commercial Share Alike 4.0 International", + "licenseId": "CC-BY-NC-SA-4.0", "seeAlso": [ - "https://jogamp.org/git/?p\u003dgluegen.git;a\u003dblob_plain;f\u003dLICENSE.txt" + "https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BlueOak-1.0.0.html", + "reference": "https://spdx.org/licenses/CC-BY-ND-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BlueOak-1.0.0.json", - "referenceNumber": 118, - "name": "Blue Oak Model License 1.0.0", - "licenseId": "BlueOak-1.0.0", + "detailsUrl": "https://spdx.org/licenses/CC-BY-ND-1.0.json", + "referenceNumber": 257, + "name": "Creative Commons Attribution No Derivatives 1.0 Generic", + "licenseId": "CC-BY-ND-1.0", "seeAlso": [ - "https://blueoakcouncil.org/license/1.0.0" + "https://creativecommons.org/licenses/by-nd/1.0/legalcode" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/Community-Spec-1.0.html", + "reference": "https://spdx.org/licenses/CC-BY-ND-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Community-Spec-1.0.json", - "referenceNumber": 119, - "name": "Community Specification License 1.0", - "licenseId": "Community-Spec-1.0", + "detailsUrl": "https://spdx.org/licenses/CC-BY-ND-2.0.json", + "referenceNumber": 204, + "name": "Creative Commons Attribution No Derivatives 2.0 Generic", + "licenseId": "CC-BY-ND-2.0", "seeAlso": [ - "https://github.com/CommunitySpecification/1.0/blob/master/1._Community_Specification_License-v1.md" + "https://creativecommons.org/licenses/by-nd/2.0/legalcode" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/GFDL-1.1-only.html", + "reference": "https://spdx.org/licenses/CC-BY-ND-2.5.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.1-only.json", - "referenceNumber": 120, - "name": "GNU Free Documentation License v1.1 only", - "licenseId": "GFDL-1.1-only", + "detailsUrl": "https://spdx.org/licenses/CC-BY-ND-2.5.json", + "referenceNumber": 72, + "name": "Creative Commons Attribution No Derivatives 2.5 Generic", + "licenseId": "CC-BY-ND-2.5", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" + "https://creativecommons.org/licenses/by-nd/2.5/legalcode" ], "isOsiApproved": false, - "isFsfLibre": true + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/RPL-1.5.html", + "reference": "https://spdx.org/licenses/CC-BY-ND-3.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/RPL-1.5.json", - "referenceNumber": 121, - "name": "Reciprocal Public License 1.5", - "licenseId": "RPL-1.5", + "detailsUrl": "https://spdx.org/licenses/CC-BY-ND-3.0.json", + "referenceNumber": 331, + "name": "Creative Commons Attribution No Derivatives 3.0 Unported", + "licenseId": "CC-BY-ND-3.0", "seeAlso": [ - "https://opensource.org/licenses/RPL-1.5" + "https://creativecommons.org/licenses/by-nd/3.0/legalcode" ], - "isOsiApproved": true + "isOsiApproved": false, + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/OFL-1.1.html", + "reference": "https://spdx.org/licenses/CC-BY-ND-3.0-DE.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OFL-1.1.json", - "referenceNumber": 122, - "name": "SIL Open Font License 1.1", - "licenseId": "OFL-1.1", + "detailsUrl": "https://spdx.org/licenses/CC-BY-ND-3.0-DE.json", + "referenceNumber": 429, + "name": "Creative Commons Attribution No Derivatives 3.0 Germany", + "licenseId": "CC-BY-ND-3.0-DE", "seeAlso": [ - "http://scripts.sil.org/cms/scripts/page.php?item_id\u003dOFL_web", - "https://opensource.org/licenses/OFL-1.1" + "https://creativecommons.org/licenses/by-nd/3.0/de/legalcode" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CPL-1.0.html", + "reference": "https://spdx.org/licenses/CC-BY-ND-4.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CPL-1.0.json", - "referenceNumber": 123, - "name": "Common Public License 1.0", - "licenseId": "CPL-1.0", + "detailsUrl": "https://spdx.org/licenses/CC-BY-ND-4.0.json", + "referenceNumber": 407, + "name": "Creative Commons Attribution No Derivatives 4.0 International", + "licenseId": "CC-BY-ND-4.0", "seeAlso": [ - "https://opensource.org/licenses/CPL-1.0" + "https://creativecommons.org/licenses/by-nd/4.0/legalcode" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false, + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/AGPL-3.0-only.html", + "reference": "https://spdx.org/licenses/CC-BY-SA-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/AGPL-3.0-only.json", - "referenceNumber": 124, - "name": "GNU Affero General Public License v3.0 only", - "licenseId": "AGPL-3.0-only", + "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-1.0.json", + "referenceNumber": 199, + "name": "Creative Commons Attribution Share Alike 1.0 Generic", + "licenseId": "CC-BY-SA-1.0", "seeAlso": [ - "https://www.gnu.org/licenses/agpl.txt", - "https://opensource.org/licenses/AGPL-3.0" + "https://creativecommons.org/licenses/by-sa/1.0/legalcode" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/gnuplot.html", + "reference": "https://spdx.org/licenses/CC-BY-SA-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/gnuplot.json", - "referenceNumber": 125, - "name": "gnuplot License", - "licenseId": "gnuplot", + "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-2.0.json", + "referenceNumber": 478, + "name": "Creative Commons Attribution Share Alike 2.0 Generic", + "licenseId": "CC-BY-SA-2.0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Gnuplot" + "https://creativecommons.org/licenses/by-sa/2.0/legalcode" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/NLPL.html", + "reference": "https://spdx.org/licenses/CC-BY-SA-2.0-UK.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NLPL.json", - "referenceNumber": 126, - "name": "No Limit Public License", - "licenseId": "NLPL", + "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-2.0-UK.json", + "referenceNumber": 361, + "name": "Creative Commons Attribution Share Alike 2.0 England and Wales", + "licenseId": "CC-BY-SA-2.0-UK", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/NLPL" + "https://creativecommons.org/licenses/by-sa/2.0/uk/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/ADSL.html", + "reference": "https://spdx.org/licenses/CC-BY-SA-2.1-JP.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/ADSL.json", - "referenceNumber": 127, - "name": "Amazon Digital Services License", - "licenseId": "ADSL", + "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-2.1-JP.json", + "referenceNumber": 461, + "name": "Creative Commons Attribution Share Alike 2.1 Japan", + "licenseId": "CC-BY-SA-2.1-JP", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/AmazonDigitalServicesLicense" + "https://creativecommons.org/licenses/by-sa/2.1/jp/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/psfrag.html", + "reference": "https://spdx.org/licenses/CC-BY-SA-2.5.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/psfrag.json", - "referenceNumber": 128, - "name": "psfrag License", - "licenseId": "psfrag", + "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-2.5.json", + "referenceNumber": 418, + "name": "Creative Commons Attribution Share Alike 2.5 Generic", + "licenseId": "CC-BY-SA-2.5", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/psfrag" + "https://creativecommons.org/licenses/by-sa/2.5/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Watcom-1.0.html", + "reference": "https://spdx.org/licenses/CC-BY-SA-3.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Watcom-1.0.json", - "referenceNumber": 129, - "name": "Sybase Open Watcom Public License 1.0", - "licenseId": "Watcom-1.0", + "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-3.0.json", + "referenceNumber": 340, + "name": "Creative Commons Attribution Share Alike 3.0 Unported", + "licenseId": "CC-BY-SA-3.0", "seeAlso": [ - "https://opensource.org/licenses/Watcom-1.0" + "https://creativecommons.org/licenses/by-sa/3.0/legalcode" ], - "isOsiApproved": true, - "isFsfLibre": false + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/SGI-B-1.0.html", + "reference": "https://spdx.org/licenses/CC-BY-SA-3.0-AT.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SGI-B-1.0.json", - "referenceNumber": 130, - "name": "SGI Free Software License B v1.0", - "licenseId": "SGI-B-1.0", + "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-3.0-AT.json", + "referenceNumber": 260, + "name": "Creative Commons Attribution Share Alike 3.0 Austria", + "licenseId": "CC-BY-SA-3.0-AT", "seeAlso": [ - "http://oss.sgi.com/projects/FreeB/SGIFreeSWLicB.1.0.html" + "https://creativecommons.org/licenses/by-sa/3.0/at/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/W3C-19980720.html", + "reference": "https://spdx.org/licenses/CC-BY-SA-3.0-DE.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/W3C-19980720.json", - "referenceNumber": 131, - "name": "W3C Software Notice and License (1998-07-20)", - "licenseId": "W3C-19980720", + "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-3.0-DE.json", + "referenceNumber": 100, + "name": "Creative Commons Attribution Share Alike 3.0 Germany", + "licenseId": "CC-BY-SA-3.0-DE", "seeAlso": [ - "http://www.w3.org/Consortium/Legal/copyright-software-19980720.html" + "https://creativecommons.org/licenses/by-sa/3.0/de/legalcode" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/NASA-1.3.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NASA-1.3.json", - "referenceNumber": 132, - "name": "NASA Open Source Agreement 1.3", - "licenseId": "NASA-1.3", - "seeAlso": [ - "http://ti.arc.nasa.gov/opensource/nosa/", - "https://opensource.org/licenses/NASA-1.3" - ], - "isOsiApproved": true, - "isFsfLibre": false - }, - { - "reference": "https://spdx.org/licenses/Fair.html", + "reference": "https://spdx.org/licenses/CC-BY-SA-4.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Fair.json", - "referenceNumber": 133, - "name": "Fair License", - "licenseId": "Fair", + "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-4.0.json", + "referenceNumber": 309, + "name": "Creative Commons Attribution Share Alike 4.0 International", + "licenseId": "CC-BY-SA-4.0", "seeAlso": [ - "http://fairlicense.org/", - "https://opensource.org/licenses/Fair" + "https://creativecommons.org/licenses/by-sa/4.0/legalcode" ], - "isOsiApproved": true + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CERN-OHL-1.1.html", + "reference": "https://spdx.org/licenses/CC-PDDC.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CERN-OHL-1.1.json", - "referenceNumber": 134, - "name": "CERN Open Hardware Licence v1.1", - "licenseId": "CERN-OHL-1.1", + "detailsUrl": "https://spdx.org/licenses/CC-PDDC.json", + "referenceNumber": 186, + "name": "Creative Commons Public Domain Dedication and Certification", + "licenseId": "CC-PDDC", "seeAlso": [ - "https://www.ohwr.org/project/licenses/wikis/cern-ohl-v1.1" + "https://creativecommons.org/licenses/publicdomain/" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/LGPL-3.0+.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/LGPL-3.0+.json", - "referenceNumber": 135, - "name": "GNU Lesser General Public License v3.0 or later", - "licenseId": "LGPL-3.0+", + "reference": "https://spdx.org/licenses/CC0-1.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/CC0-1.0.json", + "referenceNumber": 249, + "name": "Creative Commons Zero v1.0 Universal", + "licenseId": "CC0-1.0", "seeAlso": [ - "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", - "https://opensource.org/licenses/LGPL-3.0" + "https://creativecommons.org/publicdomain/zero/1.0/legalcode" ], - "isOsiApproved": true, + "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/LGPL-3.0.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/LGPL-3.0.json", - "referenceNumber": 136, - "name": "GNU Lesser General Public License v3.0 only", - "licenseId": "LGPL-3.0", + "reference": "https://spdx.org/licenses/CDDL-1.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/CDDL-1.0.json", + "referenceNumber": 242, + "name": "Common Development and Distribution License 1.0", + "licenseId": "CDDL-1.0", "seeAlso": [ - "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", - "https://opensource.org/licenses/LGPL-3.0" + "https://opensource.org/licenses/cddl1" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/BSD-2-Clause-Views.html", + "reference": "https://spdx.org/licenses/CDDL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-2-Clause-Views.json", - "referenceNumber": 137, - "name": "BSD 2-Clause with views sentence", - "licenseId": "BSD-2-Clause-Views", + "detailsUrl": "https://spdx.org/licenses/CDDL-1.1.json", + "referenceNumber": 485, + "name": "Common Development and Distribution License 1.1", + "licenseId": "CDDL-1.1", "seeAlso": [ - "http://www.freebsd.org/copyright/freebsd-license.html", - "https://people.freebsd.org/~ivoras/wine/patch-wine-nvidia.sh", - "https://github.com/protegeproject/protege/blob/master/license.txt" + "http://glassfish.java.net/public/CDDL+GPL_1_1.html", + "https://javaee.github.io/glassfish/LICENSE" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OLDAP-1.1.html", + "reference": "https://spdx.org/licenses/CDL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OLDAP-1.1.json", - "referenceNumber": 138, - "name": "Open LDAP Public License v1.1", - "licenseId": "OLDAP-1.1", + "detailsUrl": "https://spdx.org/licenses/CDL-1.0.json", + "referenceNumber": 125, + "name": "Common Documentation License 1.0", + "licenseId": "CDL-1.0", "seeAlso": [ - "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d806557a5ad59804ef3a44d5abfbe91d706b0791f" + "http://www.opensource.apple.com/cdl/", + "https://fedoraproject.org/wiki/Licensing/Common_Documentation_License", + "https://www.gnu.org/licenses/license-list.html#ACDL" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-ND-3.0-DE.html", + "reference": "https://spdx.org/licenses/CDLA-Permissive-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-ND-3.0-DE.json", - "referenceNumber": 139, - "name": "Creative Commons Attribution Non Commercial No Derivatives 3.0 Germany", - "licenseId": "CC-BY-NC-ND-3.0-DE", + "detailsUrl": "https://spdx.org/licenses/CDLA-Permissive-1.0.json", + "referenceNumber": 71, + "name": "Community Data License Agreement Permissive 1.0", + "licenseId": "CDLA-Permissive-1.0", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc-nd/3.0/de/legalcode" + "https://cdla.io/permissive-1-0" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OGDL-Taiwan-1.0.html", + "reference": "https://spdx.org/licenses/CDLA-Permissive-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OGDL-Taiwan-1.0.json", - "referenceNumber": 140, - "name": "Taiwan Open Government Data License, version 1.0", - "licenseId": "OGDL-Taiwan-1.0", + "detailsUrl": "https://spdx.org/licenses/CDLA-Permissive-2.0.json", + "referenceNumber": 386, + "name": "Community Data License Agreement Permissive 2.0", + "licenseId": "CDLA-Permissive-2.0", "seeAlso": [ - "https://data.gov.tw/license" + "https://cdla.dev/permissive-2-0" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/NGPL.html", + "reference": "https://spdx.org/licenses/CDLA-Sharing-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NGPL.json", - "referenceNumber": 141, - "name": "Nethack General Public License", - "licenseId": "NGPL", + "detailsUrl": "https://spdx.org/licenses/CDLA-Sharing-1.0.json", + "referenceNumber": 431, + "name": "Community Data License Agreement Sharing 1.0", + "licenseId": "CDLA-Sharing-1.0", "seeAlso": [ - "https://opensource.org/licenses/NGPL" + "https://cdla.io/sharing-1-0" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BSL-1.0.html", + "reference": "https://spdx.org/licenses/CECILL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSL-1.0.json", - "referenceNumber": 142, - "name": "Boost Software License 1.0", - "licenseId": "BSL-1.0", + "detailsUrl": "https://spdx.org/licenses/CECILL-1.0.json", + "referenceNumber": 20, + "name": "CeCILL Free Software License Agreement v1.0", + "licenseId": "CECILL-1.0", "seeAlso": [ - "http://www.boost.org/LICENSE_1_0.txt", - "https://opensource.org/licenses/BSL-1.0" + "http://www.cecill.info/licences/Licence_CeCILL_V1-fr.html" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Sendmail.html", + "reference": "https://spdx.org/licenses/CECILL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Sendmail.json", - "referenceNumber": 143, - "name": "Sendmail License", - "licenseId": "Sendmail", + "detailsUrl": "https://spdx.org/licenses/CECILL-1.1.json", + "referenceNumber": 92, + "name": "CeCILL Free Software License Agreement v1.1", + "licenseId": "CECILL-1.1", "seeAlso": [ - "http://www.sendmail.com/pdfs/open_source/sendmail_license.pdf", - "https://web.archive.org/web/20160322142305/https://www.sendmail.com/pdfs/open_source/sendmail_license.pdf" + "http://www.cecill.info/licences/Licence_CeCILL_V1.1-US.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OLDAP-2.4.html", + "reference": "https://spdx.org/licenses/CECILL-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OLDAP-2.4.json", - "referenceNumber": 144, - "name": "Open LDAP Public License v2.4", - "licenseId": "OLDAP-2.4", + "detailsUrl": "https://spdx.org/licenses/CECILL-2.0.json", + "referenceNumber": 476, + "name": "CeCILL Free Software License Agreement v2.0", + "licenseId": "CECILL-2.0", "seeAlso": [ - "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003dcd1284c4a91a8a380d904eee68d1583f989ed386" + "http://www.cecill.info/licences/Licence_CeCILL_V2-en.html" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CrystalStacker.html", + "reference": "https://spdx.org/licenses/CECILL-2.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CrystalStacker.json", - "referenceNumber": 145, - "name": "CrystalStacker License", - "licenseId": "CrystalStacker", + "detailsUrl": "https://spdx.org/licenses/CECILL-2.1.json", + "referenceNumber": 321, + "name": "CeCILL Free Software License Agreement v2.1", + "licenseId": "CECILL-2.1", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing:CrystalStacker?rd\u003dLicensing/CrystalStacker" + "http://www.cecill.info/licences/Licence_CeCILL_V2.1-en.html" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/blessing.html", + "reference": "https://spdx.org/licenses/CECILL-B.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/blessing.json", - "referenceNumber": 146, - "name": "SQLite Blessing", - "licenseId": "blessing", + "detailsUrl": "https://spdx.org/licenses/CECILL-B.json", + "referenceNumber": 462, + "name": "CeCILL-B Free Software License Agreement", + "licenseId": "CECILL-B", "seeAlso": [ - "https://www.sqlite.org/src/artifact/e33a4df7e32d742a?ln\u003d4-9", - "https://sqlite.org/src/artifact/df5091916dbb40e6" + "http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CC-BY-SA-4.0.html", + "reference": "https://spdx.org/licenses/CECILL-C.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-4.0.json", - "referenceNumber": 147, - "name": "Creative Commons Attribution Share Alike 4.0 International", - "licenseId": "CC-BY-SA-4.0", + "detailsUrl": "https://spdx.org/licenses/CECILL-C.json", + "referenceNumber": 360, + "name": "CeCILL-C Free Software License Agreement", + "licenseId": "CECILL-C", "seeAlso": [ - "https://creativecommons.org/licenses/by-sa/4.0/legalcode" + "http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.html" ], "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CC-BY-2.5.html", + "reference": "https://spdx.org/licenses/CERN-OHL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-2.5.json", - "referenceNumber": 148, - "name": "Creative Commons Attribution 2.5 Generic", - "licenseId": "CC-BY-2.5", + "detailsUrl": "https://spdx.org/licenses/CERN-OHL-1.1.json", + "referenceNumber": 56, + "name": "CERN Open Hardware Licence v1.1", + "licenseId": "CERN-OHL-1.1", "seeAlso": [ - "https://creativecommons.org/licenses/by/2.5/legalcode" + "https://www.ohwr.org/project/licenses/wikis/cern-ohl-v1.1" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/ICU.html", + "reference": "https://spdx.org/licenses/CERN-OHL-1.2.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/ICU.json", - "referenceNumber": 149, - "name": "ICU License", - "licenseId": "ICU", + "detailsUrl": "https://spdx.org/licenses/CERN-OHL-1.2.json", + "referenceNumber": 189, + "name": "CERN Open Hardware Licence v1.2", + "licenseId": "CERN-OHL-1.2", "seeAlso": [ - "http://source.icu-project.org/repos/icu/icu/trunk/license.html" + "https://www.ohwr.org/project/licenses/wikis/cern-ohl-v1.2" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/diffmark.html", + "reference": "https://spdx.org/licenses/CERN-OHL-P-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/diffmark.json", - "referenceNumber": 150, - "name": "diffmark license", - "licenseId": "diffmark", + "detailsUrl": "https://spdx.org/licenses/CERN-OHL-P-2.0.json", + "referenceNumber": 122, + "name": "CERN Open Hardware Licence Version 2 - Permissive", + "licenseId": "CERN-OHL-P-2.0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/diffmark" + "https://www.ohwr.org/project/cernohl/wikis/Documents/CERN-OHL-version-2" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/libpng-2.0.html", + "reference": "https://spdx.org/licenses/CERN-OHL-S-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/libpng-2.0.json", - "referenceNumber": 151, - "name": "PNG Reference Library version 2", - "licenseId": "libpng-2.0", + "detailsUrl": "https://spdx.org/licenses/CERN-OHL-S-2.0.json", + "referenceNumber": 181, + "name": "CERN Open Hardware Licence Version 2 - Strongly Reciprocal", + "licenseId": "CERN-OHL-S-2.0", "seeAlso": [ - "http://www.libpng.org/pub/png/src/libpng-LICENSE.txt" + "https://www.ohwr.org/project/cernohl/wikis/Documents/CERN-OHL-version-2" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/GPL-1.0+.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GPL-1.0+.json", - "referenceNumber": 152, - "name": "GNU General Public License v1.0 or later", - "licenseId": "GPL-1.0+", + "reference": "https://spdx.org/licenses/CERN-OHL-W-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/CERN-OHL-W-2.0.json", + "referenceNumber": 213, + "name": "CERN Open Hardware Licence Version 2 - Weakly Reciprocal", + "licenseId": "CERN-OHL-W-2.0", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/gpl-1.0-standalone.html" + "https://www.ohwr.org/project/cernohl/wikis/Documents/CERN-OHL-version-2" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/RSCPL.html", + "reference": "https://spdx.org/licenses/checkmk.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/RSCPL.json", - "referenceNumber": 153, - "name": "Ricoh Source Code Public License", - "licenseId": "RSCPL", + "detailsUrl": "https://spdx.org/licenses/checkmk.json", + "referenceNumber": 69, + "name": "Checkmk License", + "licenseId": "checkmk", "seeAlso": [ - "http://wayback.archive.org/web/20060715140826/http://www.risource.org/RPL/RPL-1.0A.shtml", - "https://opensource.org/licenses/RSCPL" + "https://github.com/libcheck/check/blob/master/checkmk/checkmk.in" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/FSFAP.html", + "reference": "https://spdx.org/licenses/ClArtistic.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/FSFAP.json", - "referenceNumber": 154, - "name": "FSF All Permissive License", - "licenseId": "FSFAP", + "detailsUrl": "https://spdx.org/licenses/ClArtistic.json", + "referenceNumber": 209, + "name": "Clarified Artistic License", + "licenseId": "ClArtistic", "seeAlso": [ - "https://www.gnu.org/prep/maintain/html_node/License-Notices-for-Other-Files.html" + "http://gianluca.dellavedova.org/2011/01/03/clarified-artistic-license/", + "http://www.ncftp.com/ncftp/doc/LICENSE.txt" ], "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/NPL-1.1.html", + "reference": "https://spdx.org/licenses/CNRI-Jython.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NPL-1.1.json", - "referenceNumber": 155, - "name": "Netscape Public License v1.1", - "licenseId": "NPL-1.1", + "detailsUrl": "https://spdx.org/licenses/CNRI-Jython.json", + "referenceNumber": 108, + "name": "CNRI Jython License", + "licenseId": "CNRI-Jython", "seeAlso": [ - "http://www.mozilla.org/MPL/NPL/1.1/" + "http://www.jython.org/license.html" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OSL-1.1.html", + "reference": "https://spdx.org/licenses/CNRI-Python.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OSL-1.1.json", - "referenceNumber": 156, - "name": "Open Software License 1.1", - "licenseId": "OSL-1.1", + "detailsUrl": "https://spdx.org/licenses/CNRI-Python.json", + "referenceNumber": 396, + "name": "CNRI Python License", + "licenseId": "CNRI-Python", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/OSL1.1" + "https://opensource.org/licenses/CNRI-Python" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/MIT-feh.html", + "reference": "https://spdx.org/licenses/CNRI-Python-GPL-Compatible.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MIT-feh.json", - "referenceNumber": 157, - "name": "feh License", - "licenseId": "MIT-feh", + "detailsUrl": "https://spdx.org/licenses/CNRI-Python-GPL-Compatible.json", + "referenceNumber": 123, + "name": "CNRI Python Open Source GPL Compatible License Agreement", + "licenseId": "CNRI-Python-GPL-Compatible", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/MIT#feh" + "http://www.python.org/download/releases/1.6.1/download_win/" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/PDDL-1.0.html", + "reference": "https://spdx.org/licenses/COIL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/PDDL-1.0.json", - "referenceNumber": 158, - "name": "Open Data Commons Public Domain Dedication \u0026 License 1.0", - "licenseId": "PDDL-1.0", + "detailsUrl": "https://spdx.org/licenses/COIL-1.0.json", + "referenceNumber": 136, + "name": "Copyfree Open Innovation License", + "licenseId": "COIL-1.0", "seeAlso": [ - "http://opendatacommons.org/licenses/pddl/1.0/", - "https://opendatacommons.org/licenses/pddl/" + "https://coil.apotheon.org/plaintext/01.0.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CERN-OHL-1.2.html", + "reference": "https://spdx.org/licenses/Community-Spec-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CERN-OHL-1.2.json", - "referenceNumber": 159, - "name": "CERN Open Hardware Licence v1.2", - "licenseId": "CERN-OHL-1.2", + "detailsUrl": "https://spdx.org/licenses/Community-Spec-1.0.json", + "referenceNumber": 183, + "name": "Community Specification License 1.0", + "licenseId": "Community-Spec-1.0", "seeAlso": [ - "https://www.ohwr.org/project/licenses/wikis/cern-ohl-v1.2" + "https://github.com/CommunitySpecification/1.0/blob/master/1._Community_Specification_License-v1.md" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-3.0.html", + "reference": "https://spdx.org/licenses/Condor-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-3.0.json", - "referenceNumber": 160, - "name": "Creative Commons Attribution 3.0 Unported", - "licenseId": "CC-BY-3.0", + "detailsUrl": "https://spdx.org/licenses/Condor-1.1.json", + "referenceNumber": 387, + "name": "Condor Public License v1.1", + "licenseId": "Condor-1.1", "seeAlso": [ - "https://creativecommons.org/licenses/by/3.0/legalcode" + "http://research.cs.wisc.edu/condor/license.html#condor", + "http://web.archive.org/web/20111123062036/http://research.cs.wisc.edu/condor/license.html#condor" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-ND-2.0.html", + "reference": "https://spdx.org/licenses/copyleft-next-0.3.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-ND-2.0.json", - "referenceNumber": 161, - "name": "Creative Commons Attribution Non Commercial No Derivatives 2.0 Generic", - "licenseId": "CC-BY-NC-ND-2.0", + "detailsUrl": "https://spdx.org/licenses/copyleft-next-0.3.0.json", + "referenceNumber": 44, + "name": "copyleft-next 0.3.0", + "licenseId": "copyleft-next-0.3.0", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc-nd/2.0/legalcode" + "https://github.com/copyleft-next/copyleft-next/blob/master/Releases/copyleft-next-0.3.0" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GFDL-1.1-or-later.html", + "reference": "https://spdx.org/licenses/copyleft-next-0.3.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.1-or-later.json", - "referenceNumber": 162, - "name": "GNU Free Documentation License v1.1 or later", - "licenseId": "GFDL-1.1-or-later", + "detailsUrl": "https://spdx.org/licenses/copyleft-next-0.3.1.json", + "referenceNumber": 359, + "name": "copyleft-next 0.3.1", + "licenseId": "copyleft-next-0.3.1", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" + "https://github.com/copyleft-next/copyleft-next/blob/master/Releases/copyleft-next-0.3.1" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Borceux.html", + "reference": "https://spdx.org/licenses/CPAL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Borceux.json", - "referenceNumber": 163, - "name": "Borceux license", - "licenseId": "Borceux", + "detailsUrl": "https://spdx.org/licenses/CPAL-1.0.json", + "referenceNumber": 467, + "name": "Common Public Attribution License 1.0", + "licenseId": "CPAL-1.0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Borceux" + "https://opensource.org/licenses/CPAL-1.0" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/NCSA.html", + "reference": "https://spdx.org/licenses/CPL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NCSA.json", - "referenceNumber": 164, - "name": "University of Illinois/NCSA Open Source License", - "licenseId": "NCSA", + "detailsUrl": "https://spdx.org/licenses/CPL-1.0.json", + "referenceNumber": 280, + "name": "Common Public License 1.0", + "licenseId": "CPL-1.0", "seeAlso": [ - "http://otm.illinois.edu/uiuc_openSource", - "https://opensource.org/licenses/NCSA" + "https://opensource.org/licenses/CPL-1.0" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Naumen.html", + "reference": "https://spdx.org/licenses/CPOL-1.02.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Naumen.json", - "referenceNumber": 165, - "name": "Naumen Public License", - "licenseId": "Naumen", + "detailsUrl": "https://spdx.org/licenses/CPOL-1.02.json", + "referenceNumber": 482, + "name": "Code Project Open License 1.02", + "licenseId": "CPOL-1.02", "seeAlso": [ - "https://opensource.org/licenses/Naumen" + "http://www.codeproject.com/info/cpol10.aspx" ], - "isOsiApproved": true + "isOsiApproved": false, + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/Caldera.html", + "reference": "https://spdx.org/licenses/Crossword.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Caldera.json", - "referenceNumber": 166, - "name": "Caldera License", - "licenseId": "Caldera", + "detailsUrl": "https://spdx.org/licenses/Crossword.json", + "referenceNumber": 53, + "name": "Crossword License", + "licenseId": "Crossword", "seeAlso": [ - "http://www.lemis.com/grog/UNIX/ancient-source-all.pdf" + "https://fedoraproject.org/wiki/Licensing/Crossword" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/PHP-3.01.html", + "reference": "https://spdx.org/licenses/CrystalStacker.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/PHP-3.01.json", - "referenceNumber": 167, - "name": "PHP License v3.01", - "licenseId": "PHP-3.01", + "detailsUrl": "https://spdx.org/licenses/CrystalStacker.json", + "referenceNumber": 141, + "name": "CrystalStacker License", + "licenseId": "CrystalStacker", "seeAlso": [ - "http://www.php.net/license/3_01.txt" + "https://fedoraproject.org/wiki/Licensing:CrystalStacker?rd\u003dLicensing/CrystalStacker" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GPL-3.0.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GPL-3.0.json", - "referenceNumber": 168, - "name": "GNU General Public License v3.0 only", - "licenseId": "GPL-3.0", + "reference": "https://spdx.org/licenses/CUA-OPL-1.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/CUA-OPL-1.0.json", + "referenceNumber": 118, + "name": "CUA Office Public License v1.0", + "licenseId": "CUA-OPL-1.0", "seeAlso": [ - "https://www.gnu.org/licenses/gpl-3.0-standalone.html", - "https://opensource.org/licenses/GPL-3.0" + "https://opensource.org/licenses/CUA-OPL-1.0" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/GFDL-1.1-no-invariants-only.html", + "reference": "https://spdx.org/licenses/Cube.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.1-no-invariants-only.json", - "referenceNumber": 169, - "name": "GNU Free Documentation License v1.1 only - no invariants", - "licenseId": "GFDL-1.1-no-invariants-only", + "detailsUrl": "https://spdx.org/licenses/Cube.json", + "referenceNumber": 232, + "name": "Cube License", + "licenseId": "Cube", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" + "https://fedoraproject.org/wiki/Licensing/Cube" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/DOC.html", + "reference": "https://spdx.org/licenses/curl.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/DOC.json", - "referenceNumber": 170, - "name": "DOC License", - "licenseId": "DOC", + "detailsUrl": "https://spdx.org/licenses/curl.json", + "referenceNumber": 346, + "name": "curl License", + "licenseId": "curl", "seeAlso": [ - "http://www.cs.wustl.edu/~schmidt/ACE-copying.html", - "https://www.dre.vanderbilt.edu/~schmidt/ACE-copying.html" + "https://github.com/bagder/curl/blob/master/COPYING" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OGC-1.0.html", + "reference": "https://spdx.org/licenses/D-FSL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OGC-1.0.json", - "referenceNumber": 171, - "name": "OGC Software License, Version 1.0", - "licenseId": "OGC-1.0", + "detailsUrl": "https://spdx.org/licenses/D-FSL-1.0.json", + "referenceNumber": 297, + "name": "Deutsche Freie Software Lizenz", + "licenseId": "D-FSL-1.0", "seeAlso": [ - "https://www.ogc.org/ogc/software/1.0" + "http://www.dipp.nrw.de/d-fsl/lizenzen/", + "http://www.dipp.nrw.de/d-fsl/index_html/lizenzen/de/D-FSL-1_0_de.txt", + "http://www.dipp.nrw.de/d-fsl/index_html/lizenzen/en/D-FSL-1_0_en.txt", + "https://www.hbz-nrw.de/produkte/open-access/lizenzen/dfsl", + "https://www.hbz-nrw.de/produkte/open-access/lizenzen/dfsl/deutsche-freie-software-lizenz", + "https://www.hbz-nrw.de/produkte/open-access/lizenzen/dfsl/german-free-software-license", + "https://www.hbz-nrw.de/produkte/open-access/lizenzen/dfsl/D-FSL-1_0_de.txt/at_download/file", + "https://www.hbz-nrw.de/produkte/open-access/lizenzen/dfsl/D-FSL-1_0_en.txt/at_download/file" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GPL-3.0-or-later.html", + "reference": "https://spdx.org/licenses/diffmark.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GPL-3.0-or-later.json", - "referenceNumber": 172, - "name": "GNU General Public License v3.0 or later", - "licenseId": "GPL-3.0-or-later", + "detailsUrl": "https://spdx.org/licenses/diffmark.json", + "referenceNumber": 37, + "name": "diffmark license", + "licenseId": "diffmark", "seeAlso": [ - "https://www.gnu.org/licenses/gpl-3.0-standalone.html", - "https://opensource.org/licenses/GPL-3.0" + "https://fedoraproject.org/wiki/Licensing/diffmark" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/SAX-PD.html", + "reference": "https://spdx.org/licenses/DL-DE-BY-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SAX-PD.json", - "referenceNumber": 173, - "name": "Sax Public Domain Notice", - "licenseId": "SAX-PD", + "detailsUrl": "https://spdx.org/licenses/DL-DE-BY-2.0.json", + "referenceNumber": 265, + "name": "Data licence Germany – attribution – version 2.0", + "licenseId": "DL-DE-BY-2.0", "seeAlso": [ - "http://www.saxproject.org/copying.html" + "https://www.govdata.de/dl-de/by-2-0" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CNRI-Python.html", + "reference": "https://spdx.org/licenses/DOC.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CNRI-Python.json", - "referenceNumber": 174, - "name": "CNRI Python License", - "licenseId": "CNRI-Python", + "detailsUrl": "https://spdx.org/licenses/DOC.json", + "referenceNumber": 453, + "name": "DOC License", + "licenseId": "DOC", "seeAlso": [ - "https://opensource.org/licenses/CNRI-Python" + "http://www.cs.wustl.edu/~schmidt/ACE-copying.html", + "https://www.dre.vanderbilt.edu/~schmidt/ACE-copying.html" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/HPND.html", + "reference": "https://spdx.org/licenses/Dotseqn.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/HPND.json", - "referenceNumber": 175, - "name": "Historical Permission Notice and Disclaimer", - "licenseId": "HPND", + "detailsUrl": "https://spdx.org/licenses/Dotseqn.json", + "referenceNumber": 48, + "name": "Dotseqn License", + "licenseId": "Dotseqn", "seeAlso": [ - "https://opensource.org/licenses/HPND" + "https://fedoraproject.org/wiki/Licensing/Dotseqn" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CECILL-C.html", + "reference": "https://spdx.org/licenses/DRL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CECILL-C.json", - "referenceNumber": 176, - "name": "CeCILL-C Free Software License Agreement", - "licenseId": "CECILL-C", + "detailsUrl": "https://spdx.org/licenses/DRL-1.0.json", + "referenceNumber": 239, + "name": "Detection Rule License 1.0", + "licenseId": "DRL-1.0", "seeAlso": [ - "http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.html" + "https://github.com/Neo23x0/sigma/blob/master/LICENSE.Detection.Rules.md" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GPL-2.0-only.html", + "reference": "https://spdx.org/licenses/DSDP.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GPL-2.0-only.json", - "referenceNumber": 177, - "name": "GNU General Public License v2.0 only", - "licenseId": "GPL-2.0-only", + "detailsUrl": "https://spdx.org/licenses/DSDP.json", + "referenceNumber": 404, + "name": "DSDP License", + "licenseId": "DSDP", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", - "https://opensource.org/licenses/GPL-2.0" + "https://fedoraproject.org/wiki/Licensing/DSDP" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/PSF-2.0.html", + "reference": "https://spdx.org/licenses/dvipdfm.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/PSF-2.0.json", - "referenceNumber": 178, - "name": "Python Software Foundation License 2.0", - "licenseId": "PSF-2.0", + "detailsUrl": "https://spdx.org/licenses/dvipdfm.json", + "referenceNumber": 388, + "name": "dvipdfm License", + "licenseId": "dvipdfm", "seeAlso": [ - "https://opensource.org/licenses/Python-2.0" + "https://fedoraproject.org/wiki/Licensing/dvipdfm" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OFL-1.1-no-RFN.html", + "reference": "https://spdx.org/licenses/ECL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OFL-1.1-no-RFN.json", - "referenceNumber": 179, - "name": "SIL Open Font License 1.1 with no Reserved Font Name", - "licenseId": "OFL-1.1-no-RFN", + "detailsUrl": "https://spdx.org/licenses/ECL-1.0.json", + "referenceNumber": 298, + "name": "Educational Community License v1.0", + "licenseId": "ECL-1.0", "seeAlso": [ - "http://scripts.sil.org/cms/scripts/page.php?item_id\u003dOFL_web", - "https://opensource.org/licenses/OFL-1.1" + "https://opensource.org/licenses/ECL-1.0" ], "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/W3C.html", + "reference": "https://spdx.org/licenses/ECL-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/W3C.json", - "referenceNumber": 180, - "name": "W3C Software Notice and License (2002-12-31)", - "licenseId": "W3C", + "detailsUrl": "https://spdx.org/licenses/ECL-2.0.json", + "referenceNumber": 35, + "name": "Educational Community License v2.0", + "licenseId": "ECL-2.0", "seeAlso": [ - "http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231.html", - "https://opensource.org/licenses/W3C" + "https://opensource.org/licenses/ECL-2.0" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/OFL-1.0-no-RFN.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OFL-1.0-no-RFN.json", - "referenceNumber": 181, - "name": "SIL Open Font License 1.0 with no Reserved Font Name", - "licenseId": "OFL-1.0-no-RFN", - "seeAlso": [ - "http://scripts.sil.org/cms/scripts/page.php?item_id\u003dOFL10_web" - ], - "isOsiApproved": false - }, - { - "reference": "https://spdx.org/licenses/xinetd.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/xinetd.json", - "referenceNumber": 182, - "name": "xinetd License", - "licenseId": "xinetd", + "reference": "https://spdx.org/licenses/eCos-2.0.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/eCos-2.0.json", + "referenceNumber": 285, + "name": "eCos license version 2.0", + "licenseId": "eCos-2.0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Xinetd_License" + "https://www.gnu.org/licenses/ecos-license.html" ], "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/OGTSL.html", + "reference": "https://spdx.org/licenses/EFL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OGTSL.json", - "referenceNumber": 183, - "name": "Open Group Test Suite License", - "licenseId": "OGTSL", + "detailsUrl": "https://spdx.org/licenses/EFL-1.0.json", + "referenceNumber": 238, + "name": "Eiffel Forum License v1.0", + "licenseId": "EFL-1.0", "seeAlso": [ - "http://www.opengroup.org/testing/downloads/The_Open_Group_TSL.txt", - "https://opensource.org/licenses/OGTSL" + "http://www.eiffel-nice.org/license/forum.txt", + "https://opensource.org/licenses/EFL-1.0" ], "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/GFDL-1.3.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.3.json", - "referenceNumber": 184, - "name": "GNU Free Documentation License v1.3", - "licenseId": "GFDL-1.3", + "reference": "https://spdx.org/licenses/EFL-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/EFL-2.0.json", + "referenceNumber": 409, + "name": "Eiffel Forum License v2.0", + "licenseId": "EFL-2.0", "seeAlso": [ - "https://www.gnu.org/licenses/fdl-1.3.txt" + "http://www.eiffel-nice.org/license/eiffel-forum-license-2.html", + "https://opensource.org/licenses/EFL-2.0" ], - "isOsiApproved": false, + "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/AGPL-1.0-only.html", + "reference": "https://spdx.org/licenses/eGenix.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/AGPL-1.0-only.json", - "referenceNumber": 185, - "name": "Affero General Public License v1.0 only", - "licenseId": "AGPL-1.0-only", + "detailsUrl": "https://spdx.org/licenses/eGenix.json", + "referenceNumber": 243, + "name": "eGenix.com Public License 1.1.0", + "licenseId": "eGenix", "seeAlso": [ - "http://www.affero.org/oagpl.html" - ], + "http://www.egenix.com/products/eGenix.com-Public-License-1.1.0.pdf", + "https://fedoraproject.org/wiki/Licensing/eGenix.com_Public_License_1.1.0" + ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/WTFPL.html", + "reference": "https://spdx.org/licenses/Elastic-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/WTFPL.json", - "referenceNumber": 186, - "name": "Do What The F*ck You Want To Public License", - "licenseId": "WTFPL", + "detailsUrl": "https://spdx.org/licenses/Elastic-2.0.json", + "referenceNumber": 275, + "name": "Elastic License 2.0", + "licenseId": "Elastic-2.0", "seeAlso": [ - "http://www.wtfpl.net/about/", - "http://sam.zoy.org/wtfpl/COPYING" + "https://www.elastic.co/licensing/elastic-license", + "https://github.com/elastic/elasticsearch/blob/master/licenses/ELASTIC-LICENSE-2.0.txt" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/W3C-20150513.html", + "reference": "https://spdx.org/licenses/Entessa.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/W3C-20150513.json", - "referenceNumber": 187, - "name": "W3C Software Notice and Document License (2015-05-13)", - "licenseId": "W3C-20150513", + "detailsUrl": "https://spdx.org/licenses/Entessa.json", + "referenceNumber": 487, + "name": "Entessa Public License v1.0", + "licenseId": "Entessa", "seeAlso": [ - "https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document" + "https://opensource.org/licenses/Entessa" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/Sleepycat.html", + "reference": "https://spdx.org/licenses/EPICS.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Sleepycat.json", - "referenceNumber": 188, - "name": "Sleepycat License", - "licenseId": "Sleepycat", + "detailsUrl": "https://spdx.org/licenses/EPICS.json", + "referenceNumber": 26, + "name": "EPICS Open License", + "licenseId": "EPICS", "seeAlso": [ - "https://opensource.org/licenses/Sleepycat" + "https://epics.anl.gov/license/open.php" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/JasPer-2.0.html", + "reference": "https://spdx.org/licenses/EPL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/JasPer-2.0.json", - "referenceNumber": 189, - "name": "JasPer License", - "licenseId": "JasPer-2.0", + "detailsUrl": "https://spdx.org/licenses/EPL-1.0.json", + "referenceNumber": 456, + "name": "Eclipse Public License 1.0", + "licenseId": "EPL-1.0", "seeAlso": [ - "http://www.ece.uvic.ca/~mdadams/jasper/LICENSE" + "http://www.eclipse.org/legal/epl-v10.html", + "https://opensource.org/licenses/EPL-1.0" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Nokia.html", + "reference": "https://spdx.org/licenses/EPL-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Nokia.json", - "referenceNumber": 190, - "name": "Nokia Open Source License", - "licenseId": "Nokia", + "detailsUrl": "https://spdx.org/licenses/EPL-2.0.json", + "referenceNumber": 24, + "name": "Eclipse Public License 2.0", + "licenseId": "EPL-2.0", "seeAlso": [ - "https://opensource.org/licenses/nokia" + "https://www.eclipse.org/legal/epl-2.0", + "https://www.opensource.org/licenses/EPL-2.0" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/LGPL-2.1+.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/LGPL-2.1+.json", - "referenceNumber": 191, - "name": "GNU Library General Public License v2.1 or later", - "licenseId": "LGPL-2.1+", + "reference": "https://spdx.org/licenses/ErlPL-1.1.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/ErlPL-1.1.json", + "referenceNumber": 109, + "name": "Erlang Public License v1.1", + "licenseId": "ErlPL-1.1", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", - "https://opensource.org/licenses/LGPL-2.1" + "http://www.erlang.org/EPLICENSE" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OGL-UK-2.0.html", + "reference": "https://spdx.org/licenses/etalab-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OGL-UK-2.0.json", - "referenceNumber": 192, - "name": "Open Government Licence v2.0", - "licenseId": "OGL-UK-2.0", + "detailsUrl": "https://spdx.org/licenses/etalab-2.0.json", + "referenceNumber": 484, + "name": "Etalab Open License 2.0", + "licenseId": "etalab-2.0", "seeAlso": [ - "http://www.nationalarchives.gov.uk/doc/open-government-licence/version/2/" + "https://github.com/DISIC/politique-de-contribution-open-source/blob/master/LICENSE.pdf", + "https://raw.githubusercontent.com/DISIC/politique-de-contribution-open-source/master/LICENSE" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/RPL-1.1.html", + "reference": "https://spdx.org/licenses/EUDatagrid.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/RPL-1.1.json", - "referenceNumber": 193, - "name": "Reciprocal Public License 1.1", - "licenseId": "RPL-1.1", + "detailsUrl": "https://spdx.org/licenses/EUDatagrid.json", + "referenceNumber": 405, + "name": "EU DataGrid Software License", + "licenseId": "EUDatagrid", "seeAlso": [ - "https://opensource.org/licenses/RPL-1.1" + "http://eu-datagrid.web.cern.ch/eu-datagrid/license.html", + "https://opensource.org/licenses/EUDatagrid" ], - "isOsiApproved": true + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/SHL-0.51.html", + "reference": "https://spdx.org/licenses/EUPL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SHL-0.51.json", - "referenceNumber": 194, - "name": "Solderpad Hardware License, Version 0.51", - "licenseId": "SHL-0.51", + "detailsUrl": "https://spdx.org/licenses/EUPL-1.0.json", + "referenceNumber": 81, + "name": "European Union Public License 1.0", + "licenseId": "EUPL-1.0", "seeAlso": [ - "https://solderpad.org/licenses/SHL-0.51/" + "http://ec.europa.eu/idabc/en/document/7330.html", + "http://ec.europa.eu/idabc/servlets/Doc027f.pdf?id\u003d31096" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/LiLiQ-P-1.1.html", + "reference": "https://spdx.org/licenses/EUPL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LiLiQ-P-1.1.json", - "referenceNumber": 195, - "name": "Licence Libre du Québec – Permissive version 1.1", - "licenseId": "LiLiQ-P-1.1", + "detailsUrl": "https://spdx.org/licenses/EUPL-1.1.json", + "referenceNumber": 21, + "name": "European Union Public License 1.1", + "licenseId": "EUPL-1.1", "seeAlso": [ - "https://forge.gouv.qc.ca/licence/fr/liliq-v1-1/", - "http://opensource.org/licenses/LiLiQ-P-1.1" + "https://joinup.ec.europa.eu/software/page/eupl/licence-eupl", + "https://joinup.ec.europa.eu/sites/default/files/custom-page/attachment/eupl1.1.-licence-en_0.pdf", + "https://opensource.org/licenses/EUPL-1.1" ], - "isOsiApproved": true + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/OpenSSL.html", + "reference": "https://spdx.org/licenses/EUPL-1.2.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OpenSSL.json", - "referenceNumber": 196, - "name": "OpenSSL License", - "licenseId": "OpenSSL", + "detailsUrl": "https://spdx.org/licenses/EUPL-1.2.json", + "referenceNumber": 420, + "name": "European Union Public License 1.2", + "licenseId": "EUPL-1.2", "seeAlso": [ - "http://www.openssl.org/source/license.html" + "https://joinup.ec.europa.eu/page/eupl-text-11-12", + "https://joinup.ec.europa.eu/sites/default/files/custom-page/attachment/eupl_v1.2_en.pdf", + "https://joinup.ec.europa.eu/sites/default/files/custom-page/attachment/2020-03/EUPL-1.2%20EN.txt", + "https://joinup.ec.europa.eu/sites/default/files/inline-files/EUPL%20v1_2%20EN(1).txt", + "http://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri\u003dCELEX:32017D0863", + "https://opensource.org/licenses/EUPL-1.2" ], - "isOsiApproved": false, + "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/C-UDA-1.0.html", + "reference": "https://spdx.org/licenses/Eurosym.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/C-UDA-1.0.json", - "referenceNumber": 197, - "name": "Computational Use of Data Agreement v1.0", - "licenseId": "C-UDA-1.0", + "detailsUrl": "https://spdx.org/licenses/Eurosym.json", + "referenceNumber": 470, + "name": "Eurosym License", + "licenseId": "Eurosym", "seeAlso": [ - "https://github.com/microsoft/Computational-Use-of-Data-Agreement/blob/master/C-UDA-1.0.md", - "https://cdla.dev/computational-use-of-data-agreement-v1-0/" + "https://fedoraproject.org/wiki/Licensing/Eurosym" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Xnet.html", + "reference": "https://spdx.org/licenses/Fair.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Xnet.json", - "referenceNumber": 198, - "name": "X.Net License", - "licenseId": "Xnet", + "detailsUrl": "https://spdx.org/licenses/Fair.json", + "referenceNumber": 177, + "name": "Fair License", + "licenseId": "Fair", "seeAlso": [ - "https://opensource.org/licenses/Xnet" + "http://fairlicense.org/", + "https://opensource.org/licenses/Fair" ], "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/APSL-2.0.html", + "reference": "https://spdx.org/licenses/FDK-AAC.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/APSL-2.0.json", - "referenceNumber": 199, - "name": "Apple Public Source License 2.0", - "licenseId": "APSL-2.0", + "detailsUrl": "https://spdx.org/licenses/FDK-AAC.json", + "referenceNumber": 32, + "name": "Fraunhofer FDK AAC Codec Library", + "licenseId": "FDK-AAC", "seeAlso": [ - "http://www.opensource.apple.com/license/apsl/" + "https://fedoraproject.org/wiki/Licensing/FDK-AAC", + "https://directory.fsf.org/wiki/License:Fdk" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/Frameworx-1.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/Frameworx-1.0.json", + "referenceNumber": 46, + "name": "Frameworx Open License 1.0", + "licenseId": "Frameworx-1.0", + "seeAlso": [ + "https://opensource.org/licenses/Frameworx-1.0" + ], + "isOsiApproved": true }, { "reference": "https://spdx.org/licenses/FreeBSD-DOC.html", "isDeprecatedLicenseId": false, "detailsUrl": "https://spdx.org/licenses/FreeBSD-DOC.json", - "referenceNumber": 200, + "referenceNumber": 40, "name": "FreeBSD Documentation License", "licenseId": "FreeBSD-DOC", "seeAlso": [ @@ -2537,1080 +2466,1544 @@ "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/NPL-1.0.html", + "reference": "https://spdx.org/licenses/FreeImage.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NPL-1.0.json", - "referenceNumber": 201, - "name": "Netscape Public License v1.0", - "licenseId": "NPL-1.0", + "detailsUrl": "https://spdx.org/licenses/FreeImage.json", + "referenceNumber": 130, + "name": "FreeImage Public License v1.0", + "licenseId": "FreeImage", "seeAlso": [ - "http://www.mozilla.org/MPL/NPL/1.0/" + "http://freeimage.sourceforge.net/freeimage-license.txt" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GPL-2.0-or-later.html", + "reference": "https://spdx.org/licenses/FSFAP.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GPL-2.0-or-later.json", - "referenceNumber": 202, - "name": "GNU General Public License v2.0 or later", - "licenseId": "GPL-2.0-or-later", + "detailsUrl": "https://spdx.org/licenses/FSFAP.json", + "referenceNumber": 120, + "name": "FSF All Permissive License", + "licenseId": "FSFAP", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", - "https://opensource.org/licenses/GPL-2.0" + "https://www.gnu.org/prep/maintain/html_node/License-Notices-for-Other-Files.html" ], - "isOsiApproved": true, + "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CECILL-2.0.html", + "reference": "https://spdx.org/licenses/FSFUL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CECILL-2.0.json", - "referenceNumber": 203, - "name": "CeCILL Free Software License Agreement v2.0", - "licenseId": "CECILL-2.0", + "detailsUrl": "https://spdx.org/licenses/FSFUL.json", + "referenceNumber": 215, + "name": "FSF Unlimited License", + "licenseId": "FSFUL", "seeAlso": [ - "http://www.cecill.info/licences/Licence_CeCILL_V2-en.html" + "https://fedoraproject.org/wiki/Licensing/FSF_Unlimited_License" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OFL-1.0.html", + "reference": "https://spdx.org/licenses/FSFULLR.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OFL-1.0.json", - "referenceNumber": 204, - "name": "SIL Open Font License 1.0", - "licenseId": "OFL-1.0", + "detailsUrl": "https://spdx.org/licenses/FSFULLR.json", + "referenceNumber": 43, + "name": "FSF Unlimited License (with License Retention)", + "licenseId": "FSFULLR", "seeAlso": [ - "http://scripts.sil.org/cms/scripts/page.php?item_id\u003dOFL10_web" + "https://fedoraproject.org/wiki/Licensing/FSF_Unlimited_License#License_Retention_Variant" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/EPL-1.0.html", + "reference": "https://spdx.org/licenses/FSFULLRWD.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/EPL-1.0.json", - "referenceNumber": 205, - "name": "Eclipse Public License 1.0", - "licenseId": "EPL-1.0", + "detailsUrl": "https://spdx.org/licenses/FSFULLRWD.json", + "referenceNumber": 253, + "name": "FSF Unlimited License (With License Retention and Warranty Disclaimer)", + "licenseId": "FSFULLRWD", "seeAlso": [ - "http://www.eclipse.org/legal/epl-v10.html", - "https://opensource.org/licenses/EPL-1.0" + "https://lists.gnu.org/archive/html/autoconf/2012-04/msg00061.html" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/NOSL.html", + "reference": "https://spdx.org/licenses/FTL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NOSL.json", - "referenceNumber": 206, - "name": "Netizen Open Source License", - "licenseId": "NOSL", + "detailsUrl": "https://spdx.org/licenses/FTL.json", + "referenceNumber": 390, + "name": "Freetype Project License", + "licenseId": "FTL", "seeAlso": [ - "http://bits.netizen.com.au/licenses/NOSL/nosl.txt" + "http://freetype.fis.uniroma2.it/FTL.TXT", + "http://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/docs/FTL.TXT", + "http://gitlab.freedesktop.org/freetype/freetype/-/raw/master/docs/FTL.TXT" ], "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/MIT-CMU.html", + "reference": "https://spdx.org/licenses/GD.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MIT-CMU.json", - "referenceNumber": 207, - "name": "CMU License", - "licenseId": "MIT-CMU", + "detailsUrl": "https://spdx.org/licenses/GD.json", + "referenceNumber": 483, + "name": "GD License", + "licenseId": "GD", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing:MIT?rd\u003dLicensing/MIT#CMU_Style", - "https://github.com/python-pillow/Pillow/blob/fffb426092c8db24a5f4b6df243a8a3c01fb63cd/LICENSE" + "https://libgd.github.io/manuals/2.3.0/files/license-txt.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CERN-OHL-S-2.0.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CERN-OHL-S-2.0.json", - "referenceNumber": 208, - "name": "CERN Open Hardware Licence Version 2 - Strongly Reciprocal", - "licenseId": "CERN-OHL-S-2.0", + "reference": "https://spdx.org/licenses/GFDL-1.1.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GFDL-1.1.json", + "referenceNumber": 355, + "name": "GNU Free Documentation License v1.1", + "licenseId": "GFDL-1.1", "seeAlso": [ - "https://www.ohwr.org/project/cernohl/wikis/Documents/CERN-OHL-version-2" + "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" ], - "isOsiApproved": true + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/SWL.html", + "reference": "https://spdx.org/licenses/GFDL-1.1-invariants-only.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SWL.json", - "referenceNumber": 209, - "name": "Scheme Widget Library (SWL) Software License Agreement", - "licenseId": "SWL", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.1-invariants-only.json", + "referenceNumber": 22, + "name": "GNU Free Documentation License v1.1 only - invariants", + "licenseId": "GFDL-1.1-invariants-only", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/SWL" + "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-SA-2.5.html", + "reference": "https://spdx.org/licenses/GFDL-1.1-invariants-or-later.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-2.5.json", - "referenceNumber": 210, - "name": "Creative Commons Attribution Non Commercial Share Alike 2.5 Generic", - "licenseId": "CC-BY-NC-SA-2.5", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.1-invariants-or-later.json", + "referenceNumber": 322, + "name": "GNU Free Documentation License v1.1 or later - invariants", + "licenseId": "GFDL-1.1-invariants-or-later", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc-sa/2.5/legalcode" + "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Zlib.html", + "reference": "https://spdx.org/licenses/GFDL-1.1-no-invariants-only.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Zlib.json", - "referenceNumber": 211, - "name": "zlib License", - "licenseId": "Zlib", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.1-no-invariants-only.json", + "referenceNumber": 450, + "name": "GNU Free Documentation License v1.1 only - no invariants", + "licenseId": "GFDL-1.1-no-invariants-only", "seeAlso": [ - "http://www.zlib.net/zlib_license.html", - "https://opensource.org/licenses/Zlib" + "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-SA-3.0.html", + "reference": "https://spdx.org/licenses/GFDL-1.1-no-invariants-or-later.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-3.0.json", - "referenceNumber": 212, - "name": "Creative Commons Attribution Non Commercial Share Alike 3.0 Unported", - "licenseId": "CC-BY-NC-SA-3.0", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.1-no-invariants-or-later.json", + "referenceNumber": 317, + "name": "GNU Free Documentation License v1.1 or later - no invariants", + "licenseId": "GFDL-1.1-no-invariants-or-later", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc-sa/3.0/legalcode" + "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/iMatix.html", + "reference": "https://spdx.org/licenses/GFDL-1.1-only.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/iMatix.json", - "referenceNumber": 213, - "name": "iMatix Standard Function Library Agreement", - "licenseId": "iMatix", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.1-only.json", + "referenceNumber": 0, + "name": "GNU Free Documentation License v1.1 only", + "licenseId": "GFDL-1.1-only", "seeAlso": [ - "http://legacy.imatix.com/html/sfl/sfl4.htm#license" + "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" ], "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CC-BY-ND-2.5.html", + "reference": "https://spdx.org/licenses/GFDL-1.1-or-later.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-ND-2.5.json", - "referenceNumber": 214, - "name": "Creative Commons Attribution No Derivatives 2.5 Generic", - "licenseId": "CC-BY-ND-2.5", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.1-or-later.json", + "referenceNumber": 156, + "name": "GNU Free Documentation License v1.1 or later", + "licenseId": "GFDL-1.1-or-later", "seeAlso": [ - "https://creativecommons.org/licenses/by-nd/2.5/legalcode" + "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" ], "isOsiApproved": false, - "isFsfLibre": false + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/PHP-3.0.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/PHP-3.0.json", - "referenceNumber": 215, - "name": "PHP License v3.0", - "licenseId": "PHP-3.0", + "reference": "https://spdx.org/licenses/GFDL-1.2.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GFDL-1.2.json", + "referenceNumber": 299, + "name": "GNU Free Documentation License v1.2", + "licenseId": "GFDL-1.2", "seeAlso": [ - "http://www.php.net/license/3_0.txt", - "https://opensource.org/licenses/PHP-3.0" + "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" ], - "isOsiApproved": true + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Barr.html", + "reference": "https://spdx.org/licenses/GFDL-1.2-invariants-only.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Barr.json", - "referenceNumber": 216, - "name": "Barr License", - "licenseId": "Barr", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.2-invariants-only.json", + "referenceNumber": 473, + "name": "GNU Free Documentation License v1.2 only - invariants", + "licenseId": "GFDL-1.2-invariants-only", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Barr" + "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/curl.html", + "reference": "https://spdx.org/licenses/GFDL-1.2-invariants-or-later.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/curl.json", - "referenceNumber": 217, - "name": "curl License", - "licenseId": "curl", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.2-invariants-or-later.json", + "referenceNumber": 460, + "name": "GNU Free Documentation License v1.2 or later - invariants", + "licenseId": "GFDL-1.2-invariants-or-later", "seeAlso": [ - "https://github.com/bagder/curl/blob/master/COPYING" + "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Zimbra-1.4.html", + "reference": "https://spdx.org/licenses/GFDL-1.2-no-invariants-only.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Zimbra-1.4.json", - "referenceNumber": 218, - "name": "Zimbra Public License v1.4", - "licenseId": "Zimbra-1.4", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.2-no-invariants-only.json", + "referenceNumber": 195, + "name": "GNU Free Documentation License v1.2 only - no invariants", + "licenseId": "GFDL-1.2-no-invariants-only", "seeAlso": [ - "http://www.zimbra.com/legal/zimbra-public-license-1-4" + "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/TOSL.html", + "reference": "https://spdx.org/licenses/GFDL-1.2-no-invariants-or-later.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/TOSL.json", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.2-no-invariants-or-later.json", "referenceNumber": 219, - "name": "Trusster Open Source License", - "licenseId": "TOSL", + "name": "GNU Free Documentation License v1.2 or later - no invariants", + "licenseId": "GFDL-1.2-no-invariants-or-later", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/TOSL" + "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GPL-3.0-with-GCC-exception.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GPL-3.0-with-GCC-exception.json", - "referenceNumber": 220, - "name": "GNU General Public License v3.0 w/GCC Runtime Library exception", - "licenseId": "GPL-3.0-with-GCC-exception", + "reference": "https://spdx.org/licenses/GFDL-1.2-only.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/GFDL-1.2-only.json", + "referenceNumber": 391, + "name": "GNU Free Documentation License v1.2 only", + "licenseId": "GFDL-1.2-only", "seeAlso": [ - "https://www.gnu.org/licenses/gcc-exception-3.1.html" + "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" ], - "isOsiApproved": true + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/AFL-1.2.html", + "reference": "https://spdx.org/licenses/GFDL-1.2-or-later.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/AFL-1.2.json", - "referenceNumber": 221, - "name": "Academic Free License v1.2", - "licenseId": "AFL-1.2", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.2-or-later.json", + "referenceNumber": 323, + "name": "GNU Free Documentation License v1.2 or later", + "licenseId": "GFDL-1.2-or-later", "seeAlso": [ - "http://opensource.linux-mirror.org/licenses/afl-1.2.txt", - "http://wayback.archive.org/web/20021204204652/http://www.opensource.org/licenses/academic.php" + "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" ], - "isOsiApproved": true, + "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/GFDL-1.2.html", + "reference": "https://spdx.org/licenses/GFDL-1.3.html", "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.2.json", - "referenceNumber": 222, - "name": "GNU Free Documentation License v1.2", - "licenseId": "GFDL-1.2", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.3.json", + "referenceNumber": 480, + "name": "GNU Free Documentation License v1.3", + "licenseId": "GFDL-1.3", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" + "https://www.gnu.org/licenses/fdl-1.3.txt" ], "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CC-BY-ND-3.0-DE.html", + "reference": "https://spdx.org/licenses/GFDL-1.3-invariants-only.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-ND-3.0-DE.json", - "referenceNumber": 223, - "name": "Creative Commons Attribution No Derivatives 3.0 Germany", - "licenseId": "CC-BY-ND-3.0-DE", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.3-invariants-only.json", + "referenceNumber": 262, + "name": "GNU Free Documentation License v1.3 only - invariants", + "licenseId": "GFDL-1.3-invariants-only", "seeAlso": [ - "https://creativecommons.org/licenses/by-nd/3.0/de/legalcode" + "https://www.gnu.org/licenses/fdl-1.3.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-SA-2.0.html", + "reference": "https://spdx.org/licenses/GFDL-1.3-invariants-or-later.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-2.0.json", - "referenceNumber": 224, - "name": "Creative Commons Attribution Non Commercial Share Alike 2.0 Generic", - "licenseId": "CC-BY-NC-SA-2.0", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.3-invariants-or-later.json", + "referenceNumber": 190, + "name": "GNU Free Documentation License v1.3 or later - invariants", + "licenseId": "GFDL-1.3-invariants-or-later", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc-sa/2.0/legalcode" + "https://www.gnu.org/licenses/fdl-1.3.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GFDL-1.3-invariants-only.html", + "reference": "https://spdx.org/licenses/GFDL-1.3-no-invariants-only.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.3-invariants-only.json", - "referenceNumber": 225, - "name": "GNU Free Documentation License v1.3 only - invariants", - "licenseId": "GFDL-1.3-invariants-only", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.3-no-invariants-only.json", + "referenceNumber": 344, + "name": "GNU Free Documentation License v1.3 only - no invariants", + "licenseId": "GFDL-1.3-no-invariants-only", "seeAlso": [ "https://www.gnu.org/licenses/fdl-1.3.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GD.html", + "reference": "https://spdx.org/licenses/GFDL-1.3-no-invariants-or-later.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GD.json", - "referenceNumber": 226, - "name": "GD License", - "licenseId": "GD", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.3-no-invariants-or-later.json", + "referenceNumber": 292, + "name": "GNU Free Documentation License v1.3 or later - no invariants", + "licenseId": "GFDL-1.3-no-invariants-or-later", "seeAlso": [ - "https://libgd.github.io/manuals/2.3.0/files/license-txt.html" + "https://www.gnu.org/licenses/fdl-1.3.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OLDAP-2.3.html", + "reference": "https://spdx.org/licenses/GFDL-1.3-only.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OLDAP-2.3.json", - "referenceNumber": 227, - "name": "Open LDAP Public License v2.3", - "licenseId": "OLDAP-2.3", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.3-only.json", + "referenceNumber": 284, + "name": "GNU Free Documentation License v1.3 only", + "licenseId": "GFDL-1.3-only", "seeAlso": [ - "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003dd32cf54a32d581ab475d23c810b0a7fbaf8d63c3" + "https://www.gnu.org/licenses/fdl-1.3.txt" ], "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Unicode-DFS-2016.html", + "reference": "https://spdx.org/licenses/GFDL-1.3-or-later.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Unicode-DFS-2016.json", - "referenceNumber": 228, - "name": "Unicode License Agreement - Data Files and Software (2016)", - "licenseId": "Unicode-DFS-2016", + "detailsUrl": "https://spdx.org/licenses/GFDL-1.3-or-later.json", + "referenceNumber": 435, + "name": "GNU Free Documentation License v1.3 or later", + "licenseId": "GFDL-1.3-or-later", "seeAlso": [ - "http://www.unicode.org/copyright.html" + "https://www.gnu.org/licenses/fdl-1.3.txt" ], - "isOsiApproved": true + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/IBM-pibs.html", + "reference": "https://spdx.org/licenses/Giftware.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/IBM-pibs.json", - "referenceNumber": 229, - "name": "IBM PowerPC Initialization and Boot Software", - "licenseId": "IBM-pibs", + "detailsUrl": "https://spdx.org/licenses/Giftware.json", + "referenceNumber": 358, + "name": "Giftware License", + "licenseId": "Giftware", "seeAlso": [ - "http://git.denx.de/?p\u003du-boot.git;a\u003dblob;f\u003darch/powerpc/cpu/ppc4xx/miiphy.c;h\u003d297155fdafa064b955e53e9832de93bfb0cfb85b;hb\u003d9fab4bf4cc077c21e43941866f3f2c196f28670d" + "http://liballeg.org/license.html#allegro-4-the-giftware-license" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/FDK-AAC.html", + "reference": "https://spdx.org/licenses/GL2PS.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/FDK-AAC.json", - "referenceNumber": 230, - "name": "Fraunhofer FDK AAC Codec Library", - "licenseId": "FDK-AAC", + "detailsUrl": "https://spdx.org/licenses/GL2PS.json", + "referenceNumber": 179, + "name": "GL2PS License", + "licenseId": "GL2PS", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/FDK-AAC", - "https://directory.fsf.org/wiki/License:Fdk" + "http://www.geuz.org/gl2ps/COPYING.GL2PS" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CATOSL-1.1.html", + "reference": "https://spdx.org/licenses/Glide.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CATOSL-1.1.json", - "referenceNumber": 231, - "name": "Computer Associates Trusted Open Source License 1.1", - "licenseId": "CATOSL-1.1", + "detailsUrl": "https://spdx.org/licenses/Glide.json", + "referenceNumber": 119, + "name": "3dfx Glide License", + "licenseId": "Glide", "seeAlso": [ - "https://opensource.org/licenses/CATOSL-1.1" + "http://www.users.on.net/~triforce/glidexp/COPYING.txt" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-ND-1.0.html", + "reference": "https://spdx.org/licenses/Glulxe.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-ND-1.0.json", - "referenceNumber": 232, - "name": "Creative Commons Attribution Non Commercial No Derivatives 1.0 Generic", - "licenseId": "CC-BY-NC-ND-1.0", + "detailsUrl": "https://spdx.org/licenses/Glulxe.json", + "referenceNumber": 465, + "name": "Glulxe License", + "licenseId": "Glulxe", "seeAlso": [ - "https://creativecommons.org/licenses/by-nd-nc/1.0/legalcode" + "https://fedoraproject.org/wiki/Licensing/Glulxe" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/DSDP.html", + "reference": "https://spdx.org/licenses/GLWTPL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/DSDP.json", - "referenceNumber": 233, - "name": "DSDP License", - "licenseId": "DSDP", + "detailsUrl": "https://spdx.org/licenses/GLWTPL.json", + "referenceNumber": 11, + "name": "Good Luck With That Public License", + "licenseId": "GLWTPL", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/DSDP" + "https://github.com/me-shaon/GLWTPL/commit/da5f6bc734095efbacb442c0b31e33a65b9d6e85" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BSD-2-Clause-Patent.html", + "reference": "https://spdx.org/licenses/gnuplot.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-2-Clause-Patent.json", - "referenceNumber": 234, - "name": "BSD-2-Clause Plus Patent License", - "licenseId": "BSD-2-Clause-Patent", + "detailsUrl": "https://spdx.org/licenses/gnuplot.json", + "referenceNumber": 170, + "name": "gnuplot License", + "licenseId": "gnuplot", "seeAlso": [ - "https://opensource.org/licenses/BSDplusPatent" + "https://fedoraproject.org/wiki/Licensing/Gnuplot" ], - "isOsiApproved": true + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/SugarCRM-1.1.3.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SugarCRM-1.1.3.json", - "referenceNumber": 235, - "name": "SugarCRM Public License v1.1.3", - "licenseId": "SugarCRM-1.1.3", + "reference": "https://spdx.org/licenses/GPL-1.0.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GPL-1.0.json", + "referenceNumber": 489, + "name": "GNU General Public License v1.0 only", + "licenseId": "GPL-1.0", "seeAlso": [ - "http://www.sugarcrm.com/crm/SPL" + "https://www.gnu.org/licenses/old-licenses/gpl-1.0-standalone.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-SA-2.0-UK.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-2.0-UK.json", - "referenceNumber": 236, - "name": "Creative Commons Attribution Non Commercial Share Alike 2.0 England and Wales", - "licenseId": "CC-BY-NC-SA-2.0-UK", + "reference": "https://spdx.org/licenses/GPL-1.0+.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GPL-1.0+.json", + "referenceNumber": 13, + "name": "GNU General Public License v1.0 or later", + "licenseId": "GPL-1.0+", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc-sa/2.0/uk/legalcode" + "https://www.gnu.org/licenses/old-licenses/gpl-1.0-standalone.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-2.5.html", + "reference": "https://spdx.org/licenses/GPL-1.0-only.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-2.5.json", - "referenceNumber": 237, - "name": "Creative Commons Attribution Non Commercial 2.5 Generic", - "licenseId": "CC-BY-NC-2.5", + "detailsUrl": "https://spdx.org/licenses/GPL-1.0-only.json", + "referenceNumber": 287, + "name": "GNU General Public License v1.0 only", + "licenseId": "GPL-1.0-only", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc/2.5/legalcode" + "https://www.gnu.org/licenses/old-licenses/gpl-1.0-standalone.html" ], - "isOsiApproved": false, - "isFsfLibre": false + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Unlicense.html", + "reference": "https://spdx.org/licenses/GPL-1.0-or-later.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Unlicense.json", - "referenceNumber": 238, - "name": "The Unlicense", - "licenseId": "Unlicense", + "detailsUrl": "https://spdx.org/licenses/GPL-1.0-or-later.json", + "referenceNumber": 428, + "name": "GNU General Public License v1.0 or later", + "licenseId": "GPL-1.0-or-later", "seeAlso": [ - "https://unlicense.org/" + "https://www.gnu.org/licenses/old-licenses/gpl-1.0-standalone.html" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/AGPL-1.0-or-later.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/AGPL-1.0-or-later.json", - "referenceNumber": 239, - "name": "Affero General Public License v1.0 or later", - "licenseId": "AGPL-1.0-or-later", + "reference": "https://spdx.org/licenses/GPL-2.0.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GPL-2.0.json", + "referenceNumber": 161, + "name": "GNU General Public License v2.0 only", + "licenseId": "GPL-2.0", "seeAlso": [ - "http://www.affero.org/oagpl.html" + "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", + "https://opensource.org/licenses/GPL-2.0" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/GL2PS.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GL2PS.json", - "referenceNumber": 240, - "name": "GL2PS License", - "licenseId": "GL2PS", + "reference": "https://spdx.org/licenses/GPL-2.0+.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GPL-2.0+.json", + "referenceNumber": 115, + "name": "GNU General Public License v2.0 or later", + "licenseId": "GPL-2.0+", "seeAlso": [ - "http://www.geuz.org/gl2ps/COPYING.GL2PS" + "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", + "https://opensource.org/licenses/GPL-2.0" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/zlib-acknowledgement.html", + "reference": "https://spdx.org/licenses/GPL-2.0-only.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/zlib-acknowledgement.json", - "referenceNumber": 241, - "name": "zlib/libpng License with Acknowledgement", - "licenseId": "zlib-acknowledgement", + "detailsUrl": "https://spdx.org/licenses/GPL-2.0-only.json", + "referenceNumber": 25, + "name": "GNU General Public License v2.0 only", + "licenseId": "GPL-2.0-only", + "seeAlso": [ + "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", + "https://opensource.org/licenses/GPL-2.0" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/GPL-2.0-or-later.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/GPL-2.0-or-later.json", + "referenceNumber": 474, + "name": "GNU General Public License v2.0 or later", + "licenseId": "GPL-2.0-or-later", + "seeAlso": [ + "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", + "https://opensource.org/licenses/GPL-2.0" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/GPL-2.0-with-autoconf-exception.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GPL-2.0-with-autoconf-exception.json", + "referenceNumber": 314, + "name": "GNU General Public License v2.0 w/Autoconf exception", + "licenseId": "GPL-2.0-with-autoconf-exception", + "seeAlso": [ + "http://ac-archive.sourceforge.net/doc/copyright.html" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/GPL-2.0-with-bison-exception.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GPL-2.0-with-bison-exception.json", + "referenceNumber": 374, + "name": "GNU General Public License v2.0 w/Bison exception", + "licenseId": "GPL-2.0-with-bison-exception", + "seeAlso": [ + "http://git.savannah.gnu.org/cgit/bison.git/tree/data/yacc.c?id\u003d193d7c7054ba7197b0789e14965b739162319b5e#n141" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/GPL-2.0-with-classpath-exception.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GPL-2.0-with-classpath-exception.json", + "referenceNumber": 15, + "name": "GNU General Public License v2.0 w/Classpath exception", + "licenseId": "GPL-2.0-with-classpath-exception", + "seeAlso": [ + "https://www.gnu.org/software/classpath/license.html" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/GPL-2.0-with-font-exception.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GPL-2.0-with-font-exception.json", + "referenceNumber": 198, + "name": "GNU General Public License v2.0 w/Font exception", + "licenseId": "GPL-2.0-with-font-exception", + "seeAlso": [ + "https://www.gnu.org/licenses/gpl-faq.html#FontException" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/GPL-2.0-with-GCC-exception.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GPL-2.0-with-GCC-exception.json", + "referenceNumber": 131, + "name": "GNU General Public License v2.0 w/GCC Runtime Library exception", + "licenseId": "GPL-2.0-with-GCC-exception", + "seeAlso": [ + "https://gcc.gnu.org/git/?p\u003dgcc.git;a\u003dblob;f\u003dgcc/libgcc1.c;h\u003d762f5143fc6eed57b6797c82710f3538aa52b40b;hb\u003dcb143a3ce4fb417c68f5fa2691a1b1b1053dfba9#l10" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/GPL-3.0.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GPL-3.0.json", + "referenceNumber": 171, + "name": "GNU General Public License v3.0 only", + "licenseId": "GPL-3.0", + "seeAlso": [ + "https://www.gnu.org/licenses/gpl-3.0-standalone.html", + "https://opensource.org/licenses/GPL-3.0" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/GPL-3.0+.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GPL-3.0+.json", + "referenceNumber": 349, + "name": "GNU General Public License v3.0 or later", + "licenseId": "GPL-3.0+", + "seeAlso": [ + "https://www.gnu.org/licenses/gpl-3.0-standalone.html", + "https://opensource.org/licenses/GPL-3.0" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/GPL-3.0-only.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/GPL-3.0-only.json", + "referenceNumber": 471, + "name": "GNU General Public License v3.0 only", + "licenseId": "GPL-3.0-only", + "seeAlso": [ + "https://www.gnu.org/licenses/gpl-3.0-standalone.html", + "https://opensource.org/licenses/GPL-3.0" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/GPL-3.0-or-later.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/GPL-3.0-or-later.json", + "referenceNumber": 394, + "name": "GNU General Public License v3.0 or later", + "licenseId": "GPL-3.0-or-later", + "seeAlso": [ + "https://www.gnu.org/licenses/gpl-3.0-standalone.html", + "https://opensource.org/licenses/GPL-3.0" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/GPL-3.0-with-autoconf-exception.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GPL-3.0-with-autoconf-exception.json", + "referenceNumber": 401, + "name": "GNU General Public License v3.0 w/Autoconf exception", + "licenseId": "GPL-3.0-with-autoconf-exception", + "seeAlso": [ + "https://www.gnu.org/licenses/autoconf-exception-3.0.html" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/GPL-3.0-with-GCC-exception.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/GPL-3.0-with-GCC-exception.json", + "referenceNumber": 148, + "name": "GNU General Public License v3.0 w/GCC Runtime Library exception", + "licenseId": "GPL-3.0-with-GCC-exception", + "seeAlso": [ + "https://www.gnu.org/licenses/gcc-exception-3.1.html" + ], + "isOsiApproved": true + }, + { + "reference": "https://spdx.org/licenses/Graphics-Gems.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/Graphics-Gems.json", + "referenceNumber": 247, + "name": "Graphics Gems License", + "licenseId": "Graphics-Gems", + "seeAlso": [ + "https://github.com/erich666/GraphicsGems/blob/master/LICENSE.md" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/gSOAP-1.3b.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/gSOAP-1.3b.json", + "referenceNumber": 305, + "name": "gSOAP Public License v1.3b", + "licenseId": "gSOAP-1.3b", + "seeAlso": [ + "http://www.cs.fsu.edu/~engelen/license.html" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/HaskellReport.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/HaskellReport.json", + "referenceNumber": 59, + "name": "Haskell Language Report License", + "licenseId": "HaskellReport", + "seeAlso": [ + "https://fedoraproject.org/wiki/Licensing/Haskell_Language_Report_License" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/Hippocratic-2.1.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/Hippocratic-2.1.json", + "referenceNumber": 290, + "name": "Hippocratic License 2.1", + "licenseId": "Hippocratic-2.1", + "seeAlso": [ + "https://firstdonoharm.dev/version/2/1/license.html", + "https://github.com/EthicalSource/hippocratic-license/blob/58c0e646d64ff6fbee275bfe2b9492f914e3ab2a/LICENSE.txt" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/HPND.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/HPND.json", + "referenceNumber": 426, + "name": "Historical Permission Notice and Disclaimer", + "licenseId": "HPND", + "seeAlso": [ + "https://opensource.org/licenses/HPND" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/HPND-export-US.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/HPND-export-US.json", + "referenceNumber": 319, + "name": "HPND with US Government export control warning", + "licenseId": "HPND-export-US", + "seeAlso": [ + "https://www.kermitproject.org/ck90.html#source" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/HPND-sell-variant.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/HPND-sell-variant.json", + "referenceNumber": 272, + "name": "Historical Permission Notice and Disclaimer - sell variant", + "licenseId": "HPND-sell-variant", + "seeAlso": [ + "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/sunrpc/auth_gss/gss_generic_token.c?h\u003dv4.19" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/HTMLTIDY.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/HTMLTIDY.json", + "referenceNumber": 303, + "name": "HTML Tidy License", + "licenseId": "HTMLTIDY", + "seeAlso": [ + "https://github.com/htacg/tidy-html5/blob/next/README/LICENSE.md" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/IBM-pibs.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/IBM-pibs.json", + "referenceNumber": 498, + "name": "IBM PowerPC Initialization and Boot Software", + "licenseId": "IBM-pibs", + "seeAlso": [ + "http://git.denx.de/?p\u003du-boot.git;a\u003dblob;f\u003darch/powerpc/cpu/ppc4xx/miiphy.c;h\u003d297155fdafa064b955e53e9832de93bfb0cfb85b;hb\u003d9fab4bf4cc077c21e43941866f3f2c196f28670d" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/ICU.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/ICU.json", + "referenceNumber": 197, + "name": "ICU License", + "licenseId": "ICU", + "seeAlso": [ + "http://source.icu-project.org/repos/icu/icu/trunk/license.html" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/IJG.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/IJG.json", + "referenceNumber": 38, + "name": "Independent JPEG Group License", + "licenseId": "IJG", + "seeAlso": [ + "http://dev.w3.org/cvsweb/Amaya/libjpeg/Attic/README?rev\u003d1.2" + ], + "isOsiApproved": false, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/IJG-short.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/IJG-short.json", + "referenceNumber": 281, + "name": "Independent JPEG Group License - short", + "licenseId": "IJG-short", + "seeAlso": [ + "https://sourceforge.net/p/xmedcon/code/ci/master/tree/libs/ljpg/" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/ImageMagick.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/ImageMagick.json", + "referenceNumber": 488, + "name": "ImageMagick License", + "licenseId": "ImageMagick", + "seeAlso": [ + "http://www.imagemagick.org/script/license.php" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/iMatix.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/iMatix.json", + "referenceNumber": 449, + "name": "iMatix Standard Function Library Agreement", + "licenseId": "iMatix", + "seeAlso": [ + "http://legacy.imatix.com/html/sfl/sfl4.htm#license" + ], + "isOsiApproved": false, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/Imlib2.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/Imlib2.json", + "referenceNumber": 258, + "name": "Imlib2 License", + "licenseId": "Imlib2", + "seeAlso": [ + "http://trac.enlightenment.org/e/browser/trunk/imlib2/COPYING", + "https://git.enlightenment.org/legacy/imlib2.git/tree/COPYING" + ], + "isOsiApproved": false, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/Info-ZIP.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/Info-ZIP.json", + "referenceNumber": 245, + "name": "Info-ZIP License", + "licenseId": "Info-ZIP", + "seeAlso": [ + "http://www.info-zip.org/license.html" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/Intel.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/Intel.json", + "referenceNumber": 423, + "name": "Intel Open Source License", + "licenseId": "Intel", + "seeAlso": [ + "https://opensource.org/licenses/Intel" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/Intel-ACPI.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/Intel-ACPI.json", + "referenceNumber": 373, + "name": "Intel ACPI Software License Agreement", + "licenseId": "Intel-ACPI", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/ZlibWithAcknowledgement" + "https://fedoraproject.org/wiki/Licensing/Intel_ACPI_Software_License_Agreement" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/TU-Berlin-1.0.html", + "reference": "https://spdx.org/licenses/Interbase-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/TU-Berlin-1.0.json", - "referenceNumber": 242, - "name": "Technische Universitaet Berlin License 1.0", - "licenseId": "TU-Berlin-1.0", + "detailsUrl": "https://spdx.org/licenses/Interbase-1.0.json", + "referenceNumber": 505, + "name": "Interbase Public License v1.0", + "licenseId": "Interbase-1.0", "seeAlso": [ - "https://github.com/swh/ladspa/blob/7bf6f3799fdba70fda297c2d8fd9f526803d9680/gsm/COPYRIGHT" + "https://web.archive.org/web/20060319014854/http://info.borland.com/devsupport/interbase/opensource/IPL.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Apache-1.0.html", + "reference": "https://spdx.org/licenses/IPA.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Apache-1.0.json", - "referenceNumber": 243, - "name": "Apache License 1.0", - "licenseId": "Apache-1.0", + "detailsUrl": "https://spdx.org/licenses/IPA.json", + "referenceNumber": 234, + "name": "IPA Font License", + "licenseId": "IPA", "seeAlso": [ - "http://www.apache.org/licenses/LICENSE-1.0" + "https://opensource.org/licenses/IPA" ], - "isOsiApproved": false, + "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Crossword.html", + "reference": "https://spdx.org/licenses/IPL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Crossword.json", - "referenceNumber": 244, - "name": "Crossword License", - "licenseId": "Crossword", + "detailsUrl": "https://spdx.org/licenses/IPL-1.0.json", + "referenceNumber": 447, + "name": "IBM Public License v1.0", + "licenseId": "IPL-1.0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Crossword" + "https://opensource.org/licenses/IPL-1.0" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/HPND-sell-variant.html", + "reference": "https://spdx.org/licenses/ISC.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/HPND-sell-variant.json", - "referenceNumber": 245, - "name": "Historical Permission Notice and Disclaimer - sell variant", - "licenseId": "HPND-sell-variant", + "detailsUrl": "https://spdx.org/licenses/ISC.json", + "referenceNumber": 62, + "name": "ISC License", + "licenseId": "ISC", "seeAlso": [ - "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/sunrpc/auth_gss/gss_generic_token.c?h\u003dv4.19" + "https://www.isc.org/licenses/", + "https://www.isc.org/downloads/software-support-policy/isc-license/", + "https://opensource.org/licenses/ISC" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/BSD-2-Clause.html", + "reference": "https://spdx.org/licenses/Jam.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-2-Clause.json", - "referenceNumber": 246, - "name": "BSD 2-Clause \"Simplified\" License", - "licenseId": "BSD-2-Clause", + "detailsUrl": "https://spdx.org/licenses/Jam.json", + "referenceNumber": 135, + "name": "Jam License", + "licenseId": "Jam", "seeAlso": [ - "https://opensource.org/licenses/BSD-2-Clause" + "https://www.boost.org/doc/libs/1_35_0/doc/html/jam.html", + "https://web.archive.org/web/20160330173339/https://swarm.workshop.perforce.com/files/guest/perforce_software/jam/src/README" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/CC-BY-SA-3.0-DE.html", + "reference": "https://spdx.org/licenses/JasPer-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-3.0-DE.json", - "referenceNumber": 247, - "name": "Creative Commons Attribution Share Alike 3.0 Germany", - "licenseId": "CC-BY-SA-3.0-DE", + "detailsUrl": "https://spdx.org/licenses/JasPer-2.0.json", + "referenceNumber": 289, + "name": "JasPer License", + "licenseId": "JasPer-2.0", "seeAlso": [ - "https://creativecommons.org/licenses/by-sa/3.0/de/legalcode" + "http://www.ece.uvic.ca/~mdadams/jasper/LICENSE" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/AMPAS.html", + "reference": "https://spdx.org/licenses/JPNIC.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/AMPAS.json", - "referenceNumber": 248, - "name": "Academy of Motion Picture Arts and Sciences BSD", - "licenseId": "AMPAS", + "detailsUrl": "https://spdx.org/licenses/JPNIC.json", + "referenceNumber": 440, + "name": "Japan Network Information Center License", + "licenseId": "JPNIC", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/BSD#AMPASBSD" + "https://gitlab.isc.org/isc-projects/bind9/blob/master/COPYRIGHT#L366" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/YPL-1.1.html", + "reference": "https://spdx.org/licenses/JSON.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/YPL-1.1.json", - "referenceNumber": 249, - "name": "Yahoo! Public License v1.1", - "licenseId": "YPL-1.1", + "detailsUrl": "https://spdx.org/licenses/JSON.json", + "referenceNumber": 174, + "name": "JSON License", + "licenseId": "JSON", "seeAlso": [ - "http://www.zimbra.com/license/yahoo_public_license_1.1.html" + "http://www.json.org/license.html" ], "isOsiApproved": false, - "isFsfLibre": true + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-SA-4.0.html", + "reference": "https://spdx.org/licenses/Knuth-CTAN.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-4.0.json", - "referenceNumber": 250, - "name": "Creative Commons Attribution Non Commercial Share Alike 4.0 International", - "licenseId": "CC-BY-NC-SA-4.0", + "detailsUrl": "https://spdx.org/licenses/Knuth-CTAN.json", + "referenceNumber": 76, + "name": "Knuth CTAN License", + "licenseId": "Knuth-CTAN", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode" + "https://ctan.org/license/knuth" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Glide.html", + "reference": "https://spdx.org/licenses/LAL-1.2.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Glide.json", - "referenceNumber": 251, - "name": "3dfx Glide License", - "licenseId": "Glide", + "detailsUrl": "https://spdx.org/licenses/LAL-1.2.json", + "referenceNumber": 185, + "name": "Licence Art Libre 1.2", + "licenseId": "LAL-1.2", "seeAlso": [ - "http://www.users.on.net/~triforce/glidexp/COPYING.txt" + "http://artlibre.org/licence/lal/licence-art-libre-12/" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Artistic-1.0.html", + "reference": "https://spdx.org/licenses/LAL-1.3.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Artistic-1.0.json", - "referenceNumber": 252, - "name": "Artistic License 1.0", - "licenseId": "Artistic-1.0", + "detailsUrl": "https://spdx.org/licenses/LAL-1.3.json", + "referenceNumber": 362, + "name": "Licence Art Libre 1.3", + "licenseId": "LAL-1.3", "seeAlso": [ - "https://opensource.org/licenses/Artistic-1.0" + "https://artlibre.org/" ], - "isOsiApproved": true, - "isFsfLibre": false + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OSL-1.0.html", + "reference": "https://spdx.org/licenses/Latex2e.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OSL-1.0.json", - "referenceNumber": 253, - "name": "Open Software License 1.0", - "licenseId": "OSL-1.0", + "detailsUrl": "https://spdx.org/licenses/Latex2e.json", + "referenceNumber": 231, + "name": "Latex2e License", + "licenseId": "Latex2e", "seeAlso": [ - "https://opensource.org/licenses/OSL-1.0" + "https://fedoraproject.org/wiki/Licensing/Latex2e" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/0BSD.html", + "reference": "https://spdx.org/licenses/Leptonica.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/0BSD.json", - "referenceNumber": 254, - "name": "BSD Zero Clause License", - "licenseId": "0BSD", + "detailsUrl": "https://spdx.org/licenses/Leptonica.json", + "referenceNumber": 427, + "name": "Leptonica License", + "licenseId": "Leptonica", "seeAlso": [ - "http://landley.net/toybox/license.html", - "https://opensource.org/licenses/0BSD" + "https://fedoraproject.org/wiki/Licensing/Leptonica" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/LGPL-2.0.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/LGPL-2.0.json", + "referenceNumber": 495, + "name": "GNU Library General Public License v2 only", + "licenseId": "LGPL-2.0", + "seeAlso": [ + "https://www.gnu.org/licenses/old-licenses/lgpl-2.0-standalone.html" ], "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/OLDAP-2.7.html", + "reference": "https://spdx.org/licenses/LGPL-2.0+.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/LGPL-2.0+.json", + "referenceNumber": 216, + "name": "GNU Library General Public License v2 or later", + "licenseId": "LGPL-2.0+", + "seeAlso": [ + "https://www.gnu.org/licenses/old-licenses/lgpl-2.0-standalone.html" + ], + "isOsiApproved": true + }, + { + "reference": "https://spdx.org/licenses/LGPL-2.0-only.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OLDAP-2.7.json", - "referenceNumber": 255, - "name": "Open LDAP Public License v2.7", - "licenseId": "OLDAP-2.7", + "detailsUrl": "https://spdx.org/licenses/LGPL-2.0-only.json", + "referenceNumber": 83, + "name": "GNU Library General Public License v2 only", + "licenseId": "LGPL-2.0-only", "seeAlso": [ - "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d47c2415c1df81556eeb39be6cad458ef87c534a2" + "https://www.gnu.org/licenses/old-licenses/lgpl-2.0-standalone.html" ], - "isOsiApproved": false, + "isOsiApproved": true + }, + { + "reference": "https://spdx.org/licenses/LGPL-2.0-or-later.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/LGPL-2.0-or-later.json", + "referenceNumber": 27, + "name": "GNU Library General Public License v2 or later", + "licenseId": "LGPL-2.0-or-later", + "seeAlso": [ + "https://www.gnu.org/licenses/old-licenses/lgpl-2.0-standalone.html" + ], + "isOsiApproved": true + }, + { + "reference": "https://spdx.org/licenses/LGPL-2.1.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/LGPL-2.1.json", + "referenceNumber": 503, + "name": "GNU Lesser General Public License v2.1 only", + "licenseId": "LGPL-2.1", + "seeAlso": [ + "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", + "https://opensource.org/licenses/LGPL-2.1" + ], + "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/TAPR-OHL-1.0.html", + "reference": "https://spdx.org/licenses/LGPL-2.1+.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/LGPL-2.1+.json", + "referenceNumber": 436, + "name": "GNU Lesser General Public License v2.1 or later", + "licenseId": "LGPL-2.1+", + "seeAlso": [ + "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", + "https://opensource.org/licenses/LGPL-2.1" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/LGPL-2.1-only.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/TAPR-OHL-1.0.json", - "referenceNumber": 256, - "name": "TAPR Open Hardware License v1.0", - "licenseId": "TAPR-OHL-1.0", + "detailsUrl": "https://spdx.org/licenses/LGPL-2.1-only.json", + "referenceNumber": 172, + "name": "GNU Lesser General Public License v2.1 only", + "licenseId": "LGPL-2.1-only", "seeAlso": [ - "https://www.tapr.org/OHL" + "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", + "https://opensource.org/licenses/LGPL-2.1" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Adobe-2006.html", + "reference": "https://spdx.org/licenses/LGPL-2.1-or-later.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Adobe-2006.json", - "referenceNumber": 257, - "name": "Adobe Systems Incorporated Source Code License Agreement", - "licenseId": "Adobe-2006", + "detailsUrl": "https://spdx.org/licenses/LGPL-2.1-or-later.json", + "referenceNumber": 367, + "name": "GNU Lesser General Public License v2.1 or later", + "licenseId": "LGPL-2.1-or-later", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/AdobeLicense" + "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", + "https://opensource.org/licenses/LGPL-2.1" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/AFL-1.1.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/AFL-1.1.json", - "referenceNumber": 258, - "name": "Academic Free License v1.1", - "licenseId": "AFL-1.1", + "reference": "https://spdx.org/licenses/LGPL-3.0.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/LGPL-3.0.json", + "referenceNumber": 506, + "name": "GNU Lesser General Public License v3.0 only", + "licenseId": "LGPL-3.0", "seeAlso": [ - "http://opensource.linux-mirror.org/licenses/afl-1.1.txt", - "http://wayback.archive.org/web/20021004124254/http://www.opensource.org/licenses/academic.php" + "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", + "https://www.gnu.org/licenses/lgpl+gpl-3.0.txt", + "https://opensource.org/licenses/LGPL-3.0" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Spencer-94.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Spencer-94.json", - "referenceNumber": 259, - "name": "Spencer License 94", - "licenseId": "Spencer-94", + "reference": "https://spdx.org/licenses/LGPL-3.0+.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/LGPL-3.0+.json", + "referenceNumber": 176, + "name": "GNU Lesser General Public License v3.0 or later", + "licenseId": "LGPL-3.0+", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Henry_Spencer_Reg-Ex_Library_License" + "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", + "https://www.gnu.org/licenses/lgpl+gpl-3.0.txt", + "https://opensource.org/licenses/LGPL-3.0" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/XSkat.html", + "reference": "https://spdx.org/licenses/LGPL-3.0-only.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/XSkat.json", - "referenceNumber": 260, - "name": "XSkat License", - "licenseId": "XSkat", + "detailsUrl": "https://spdx.org/licenses/LGPL-3.0-only.json", + "referenceNumber": 313, + "name": "GNU Lesser General Public License v3.0 only", + "licenseId": "LGPL-3.0-only", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/XSkat_License" + "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", + "https://www.gnu.org/licenses/lgpl+gpl-3.0.txt", + "https://opensource.org/licenses/LGPL-3.0" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Newsletr.html", + "reference": "https://spdx.org/licenses/LGPL-3.0-or-later.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Newsletr.json", - "referenceNumber": 261, - "name": "Newsletr License", - "licenseId": "Newsletr", + "detailsUrl": "https://spdx.org/licenses/LGPL-3.0-or-later.json", + "referenceNumber": 113, + "name": "GNU Lesser General Public License v3.0 or later", + "licenseId": "LGPL-3.0-or-later", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Newsletr" + "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", + "https://www.gnu.org/licenses/lgpl+gpl-3.0.txt", + "https://opensource.org/licenses/LGPL-3.0" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CC-BY-SA-2.0.html", + "reference": "https://spdx.org/licenses/LGPLLR.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-2.0.json", - "referenceNumber": 262, - "name": "Creative Commons Attribution Share Alike 2.0 Generic", - "licenseId": "CC-BY-SA-2.0", + "detailsUrl": "https://spdx.org/licenses/LGPLLR.json", + "referenceNumber": 137, + "name": "Lesser General Public License For Linguistic Resources", + "licenseId": "LGPLLR", "seeAlso": [ - "https://creativecommons.org/licenses/by-sa/2.0/legalcode" + "http://www-igm.univ-mlv.fr/~unitex/lgpllr.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/ZPL-1.1.html", + "reference": "https://spdx.org/licenses/Libpng.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/ZPL-1.1.json", - "referenceNumber": 263, - "name": "Zope Public License 1.1", - "licenseId": "ZPL-1.1", + "detailsUrl": "https://spdx.org/licenses/Libpng.json", + "referenceNumber": 392, + "name": "libpng License", + "licenseId": "Libpng", "seeAlso": [ - "http://old.zope.org/Resources/License/ZPL-1.1" + "http://www.libpng.org/pub/png/src/libpng-LICENSE.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GFDL-1.3-only.html", + "reference": "https://spdx.org/licenses/libpng-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.3-only.json", - "referenceNumber": 264, - "name": "GNU Free Documentation License v1.3 only", - "licenseId": "GFDL-1.3-only", + "detailsUrl": "https://spdx.org/licenses/libpng-2.0.json", + "referenceNumber": 308, + "name": "PNG Reference Library version 2", + "licenseId": "libpng-2.0", "seeAlso": [ - "https://www.gnu.org/licenses/fdl-1.3.txt" + "http://www.libpng.org/pub/png/src/libpng-LICENSE.txt" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/EUPL-1.0.html", + "reference": "https://spdx.org/licenses/libselinux-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/EUPL-1.0.json", - "referenceNumber": 265, - "name": "European Union Public License 1.0", - "licenseId": "EUPL-1.0", + "detailsUrl": "https://spdx.org/licenses/libselinux-1.0.json", + "referenceNumber": 372, + "name": "libselinux public domain notice", + "licenseId": "libselinux-1.0", "seeAlso": [ - "http://ec.europa.eu/idabc/en/document/7330.html", - "http://ec.europa.eu/idabc/servlets/Doc027f.pdf?id\u003d31096" + "https://github.com/SELinuxProject/selinux/blob/master/libselinux/LICENSE" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BSD-3-Clause-Clear.html", + "reference": "https://spdx.org/licenses/libtiff.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-Clear.json", - "referenceNumber": 266, - "name": "BSD 3-Clause Clear License", - "licenseId": "BSD-3-Clause-Clear", + "detailsUrl": "https://spdx.org/licenses/libtiff.json", + "referenceNumber": 240, + "name": "libtiff License", + "licenseId": "libtiff", "seeAlso": [ - "http://labs.metacarta.com/license-explanation.html#license" + "https://fedoraproject.org/wiki/Licensing/libtiff" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GPL-1.0-or-later.html", + "reference": "https://spdx.org/licenses/libutil-David-Nugent.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GPL-1.0-or-later.json", - "referenceNumber": 267, - "name": "GNU General Public License v1.0 or later", - "licenseId": "GPL-1.0-or-later", + "detailsUrl": "https://spdx.org/licenses/libutil-David-Nugent.json", + "referenceNumber": 150, + "name": "libutil David Nugent License", + "licenseId": "libutil-David-Nugent", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/gpl-1.0-standalone.html" + "http://web.mit.edu/freebsd/head/lib/libutil/login_ok.3", + "https://cgit.freedesktop.org/libbsd/tree/man/setproctitle.3bsd" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CNRI-Jython.html", + "reference": "https://spdx.org/licenses/LiLiQ-P-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CNRI-Jython.json", - "referenceNumber": 268, - "name": "CNRI Jython License", - "licenseId": "CNRI-Jython", + "detailsUrl": "https://spdx.org/licenses/LiLiQ-P-1.1.json", + "referenceNumber": 430, + "name": "Licence Libre du Québec – Permissive version 1.1", + "licenseId": "LiLiQ-P-1.1", "seeAlso": [ - "http://www.jython.org/license.html" + "https://forge.gouv.qc.ca/licence/fr/liliq-v1-1/", + "http://opensource.org/licenses/LiLiQ-P-1.1" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/Adobe-Glyph.html", + "reference": "https://spdx.org/licenses/LiLiQ-R-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Adobe-Glyph.json", - "referenceNumber": 269, - "name": "Adobe Glyph List License", - "licenseId": "Adobe-Glyph", + "detailsUrl": "https://spdx.org/licenses/LiLiQ-R-1.1.json", + "referenceNumber": 266, + "name": "Licence Libre du Québec – Réciprocité version 1.1", + "licenseId": "LiLiQ-R-1.1", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/MIT#AdobeGlyph" + "https://www.forge.gouv.qc.ca/participez/licence-logicielle/licence-libre-du-quebec-liliq-en-francais/licence-libre-du-quebec-reciprocite-liliq-r-v1-1/", + "http://opensource.org/licenses/LiLiQ-R-1.1" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/GPL-2.0-with-bison-exception.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GPL-2.0-with-bison-exception.json", - "referenceNumber": 270, - "name": "GNU General Public License v2.0 w/Bison exception", - "licenseId": "GPL-2.0-with-bison-exception", + "reference": "https://spdx.org/licenses/LiLiQ-Rplus-1.1.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/LiLiQ-Rplus-1.1.json", + "referenceNumber": 211, + "name": "Licence Libre du Québec – Réciprocité forte version 1.1", + "licenseId": "LiLiQ-Rplus-1.1", "seeAlso": [ - "http://git.savannah.gnu.org/cgit/bison.git/tree/data/yacc.c?id\u003d193d7c7054ba7197b0789e14965b739162319b5e#n141" + "https://www.forge.gouv.qc.ca/participez/licence-logicielle/licence-libre-du-quebec-liliq-en-francais/licence-libre-du-quebec-reciprocite-forte-liliq-r-v1-1/", + "http://opensource.org/licenses/LiLiQ-Rplus-1.1" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/MITNFA.html", + "reference": "https://spdx.org/licenses/Linux-man-pages-copyleft.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MITNFA.json", - "referenceNumber": 271, - "name": "MIT +no-false-attribs license", - "licenseId": "MITNFA", + "detailsUrl": "https://spdx.org/licenses/Linux-man-pages-copyleft.json", + "referenceNumber": 3, + "name": "Linux man-pages Copyleft", + "licenseId": "Linux-man-pages-copyleft", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/MITNFA" + "https://www.kernel.org/doc/man-pages/licenses.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/APSL-1.0.html", + "reference": "https://spdx.org/licenses/Linux-OpenIB.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/APSL-1.0.json", - "referenceNumber": 272, - "name": "Apple Public Source License 1.0", - "licenseId": "APSL-1.0", + "detailsUrl": "https://spdx.org/licenses/Linux-OpenIB.json", + "referenceNumber": 175, + "name": "Linux Kernel Variant of OpenIB.org license", + "licenseId": "Linux-OpenIB", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Apple_Public_Source_License_1.0" + "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/infiniband/core/sa.h" ], - "isOsiApproved": true, - "isFsfLibre": false + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/JPNIC.html", + "reference": "https://spdx.org/licenses/LOOP.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/JPNIC.json", - "referenceNumber": 273, - "name": "Japan Network Information Center License", - "licenseId": "JPNIC", + "detailsUrl": "https://spdx.org/licenses/LOOP.json", + "referenceNumber": 325, + "name": "Common Lisp LOOP License", + "licenseId": "LOOP", "seeAlso": [ - "https://gitlab.isc.org/isc-projects/bind9/blob/master/COPYRIGHT#L366" + "https://gitlab.com/embeddable-common-lisp/ecl/-/blob/develop/src/lsp/loop.lsp", + "http://git.savannah.gnu.org/cgit/gcl.git/tree/gcl/lsp/gcl_loop.lsp?h\u003dVersion_2_6_13pre", + "https://sourceforge.net/p/sbcl/sbcl/ci/master/tree/src/code/loop.lisp", + "https://github.com/cl-adams/adams/blob/master/LICENSE.md", + "https://github.com/blakemcbride/eclipse-lisp/blob/master/lisp/loop.lisp", + "https://gitlab.common-lisp.net/cmucl/cmucl/-/blob/master/src/code/loop.lisp" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BSD-Protection.html", + "reference": "https://spdx.org/licenses/LPL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-Protection.json", - "referenceNumber": 274, - "name": "BSD Protection License", - "licenseId": "BSD-Protection", + "detailsUrl": "https://spdx.org/licenses/LPL-1.0.json", + "referenceNumber": 307, + "name": "Lucent Public License Version 1.0", + "licenseId": "LPL-1.0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/BSD_Protection_License" + "https://opensource.org/licenses/LPL-1.0" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/OGL-UK-1.0.html", + "reference": "https://spdx.org/licenses/LPL-1.02.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OGL-UK-1.0.json", - "referenceNumber": 275, - "name": "Open Government Licence v1.0", - "licenseId": "OGL-UK-1.0", + "detailsUrl": "https://spdx.org/licenses/LPL-1.02.json", + "referenceNumber": 339, + "name": "Lucent Public License v1.02", + "licenseId": "LPL-1.02", "seeAlso": [ - "http://www.nationalarchives.gov.uk/doc/open-government-licence/version/1/" + "http://plan9.bell-labs.com/plan9/license.html", + "https://opensource.org/licenses/LPL-1.02" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/MulanPSL-2.0.html", + "reference": "https://spdx.org/licenses/LPPL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MulanPSL-2.0.json", - "referenceNumber": 276, - "name": "Mulan Permissive Software License, Version 2", - "licenseId": "MulanPSL-2.0", + "detailsUrl": "https://spdx.org/licenses/LPPL-1.0.json", + "referenceNumber": 252, + "name": "LaTeX Project Public License v1.0", + "licenseId": "LPPL-1.0", "seeAlso": [ - "https://license.coscl.org.cn/MulanPSL2/" + "http://www.latex-project.org/lppl/lppl-1-0.txt" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/ImageMagick.html", + "reference": "https://spdx.org/licenses/LPPL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/ImageMagick.json", - "referenceNumber": 277, - "name": "ImageMagick License", - "licenseId": "ImageMagick", + "detailsUrl": "https://spdx.org/licenses/LPPL-1.1.json", + "referenceNumber": 366, + "name": "LaTeX Project Public License v1.1", + "licenseId": "LPPL-1.1", "seeAlso": [ - "http://www.imagemagick.org/script/license.php" + "http://www.latex-project.org/lppl/lppl-1-1.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OSL-2.0.html", + "reference": "https://spdx.org/licenses/LPPL-1.2.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OSL-2.0.json", - "referenceNumber": 278, - "name": "Open Software License 2.0", - "licenseId": "OSL-2.0", + "detailsUrl": "https://spdx.org/licenses/LPPL-1.2.json", + "referenceNumber": 327, + "name": "LaTeX Project Public License v1.2", + "licenseId": "LPPL-1.2", "seeAlso": [ - "http://web.archive.org/web/20041020171434/http://www.rosenlaw.com/osl2.0.html" + "http://www.latex-project.org/lppl/lppl-1-2.txt" ], - "isOsiApproved": true, + "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/OLDAP-2.0.html", + "reference": "https://spdx.org/licenses/LPPL-1.3a.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OLDAP-2.0.json", - "referenceNumber": 279, - "name": "Open LDAP Public License v2.0 (or possibly 2.0A and 2.0B)", - "licenseId": "OLDAP-2.0", + "detailsUrl": "https://spdx.org/licenses/LPPL-1.3a.json", + "referenceNumber": 64, + "name": "LaTeX Project Public License v1.3a", + "licenseId": "LPPL-1.3a", "seeAlso": [ - "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003dcbf50f4e1185a21abd4c0a54d3f4341fe28f36ea" + "http://www.latex-project.org/lppl/lppl-1-3a.txt" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/bzip2-1.0.6.html", + "reference": "https://spdx.org/licenses/LPPL-1.3c.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/bzip2-1.0.6.json", - "referenceNumber": 280, - "name": "bzip2 and libbzip2 License v1.0.6", - "licenseId": "bzip2-1.0.6", + "detailsUrl": "https://spdx.org/licenses/LPPL-1.3c.json", + "referenceNumber": 19, + "name": "LaTeX Project Public License v1.3c", + "licenseId": "LPPL-1.3c", "seeAlso": [ - "https://sourceware.org/git/?p\u003dbzip2.git;a\u003dblob;f\u003dLICENSE;hb\u003dbzip2-1.0.6", - "http://bzip.org/1.0.5/bzip2-manual-1.0.5.html" + "http://www.latex-project.org/lppl/lppl-1-3c.txt", + "https://opensource.org/licenses/LPPL-1.3c" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/Eurosym.html", + "reference": "https://spdx.org/licenses/LZMA-SDK-9.11-to-9.20.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Eurosym.json", - "referenceNumber": 281, - "name": "Eurosym License", - "licenseId": "Eurosym", + "detailsUrl": "https://spdx.org/licenses/LZMA-SDK-9.11-to-9.20.json", + "referenceNumber": 96, + "name": "LZMA SDK License (versions 9.11 to 9.20)", + "licenseId": "LZMA-SDK-9.11-to-9.20", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Eurosym" + "https://www.7-zip.org/sdk.html", + "https://sourceforge.net/projects/sevenzip/files/LZMA%20SDK/" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/JSON.html", + "reference": "https://spdx.org/licenses/LZMA-SDK-9.22.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/JSON.json", - "referenceNumber": 282, - "name": "JSON License", - "licenseId": "JSON", + "detailsUrl": "https://spdx.org/licenses/LZMA-SDK-9.22.json", + "referenceNumber": 31, + "name": "LZMA SDK License (versions 9.22 and beyond)", + "licenseId": "LZMA-SDK-9.22", "seeAlso": [ - "http://www.json.org/license.html" + "https://www.7-zip.org/sdk.html", + "https://sourceforge.net/projects/sevenzip/files/LZMA%20SDK/" ], - "isOsiApproved": false, - "isFsfLibre": false + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-ND-3.0-IGO.html", + "reference": "https://spdx.org/licenses/MakeIndex.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-ND-3.0-IGO.json", - "referenceNumber": 283, - "name": "Creative Commons Attribution Non Commercial No Derivatives 3.0 IGO", - "licenseId": "CC-BY-NC-ND-3.0-IGO", + "detailsUrl": "https://spdx.org/licenses/MakeIndex.json", + "referenceNumber": 158, + "name": "MakeIndex License", + "licenseId": "MakeIndex", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc-nd/3.0/igo/legalcode" + "https://fedoraproject.org/wiki/Licensing/MakeIndex" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/LPL-1.0.html", + "reference": "https://spdx.org/licenses/Minpack.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LPL-1.0.json", - "referenceNumber": 284, - "name": "Lucent Public License Version 1.0", - "licenseId": "LPL-1.0", + "detailsUrl": "https://spdx.org/licenses/Minpack.json", + "referenceNumber": 508, + "name": "Minpack License", + "licenseId": "Minpack", "seeAlso": [ - "https://opensource.org/licenses/LPL-1.0" + "http://www.netlib.org/minpack/disclaimer", + "https://gitlab.com/libeigen/eigen/-/blob/master/COPYING.MINPACK" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/AGPL-1.0.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/AGPL-1.0.json", - "referenceNumber": 285, - "name": "Affero General Public License v1.0", - "licenseId": "AGPL-1.0", + "reference": "https://spdx.org/licenses/MirOS.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/MirOS.json", + "referenceNumber": 479, + "name": "The MirOS Licence", + "licenseId": "MirOS", "seeAlso": [ - "http://www.affero.org/oagpl.html" + "https://opensource.org/licenses/MirOS" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/CECILL-2.1.html", + "reference": "https://spdx.org/licenses/MIT.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CECILL-2.1.json", - "referenceNumber": 286, - "name": "CeCILL Free Software License Agreement v2.1", - "licenseId": "CECILL-2.1", + "detailsUrl": "https://spdx.org/licenses/MIT.json", + "referenceNumber": 111, + "name": "MIT License", + "licenseId": "MIT", "seeAlso": [ - "http://www.cecill.info/licences/Licence_CeCILL_V2.1-en.html" + "https://opensource.org/licenses/MIT" ], - "isOsiApproved": true + "isOsiApproved": true, + "isFsfLibre": true }, { "reference": "https://spdx.org/licenses/MIT-0.html", "isDeprecatedLicenseId": false, "detailsUrl": "https://spdx.org/licenses/MIT-0.json", - "referenceNumber": 287, + "referenceNumber": 320, "name": "MIT No Attribution", "licenseId": "MIT-0", "seeAlso": [ @@ -3621,909 +4014,919 @@ "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/Spencer-99.html", + "reference": "https://spdx.org/licenses/MIT-advertising.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Spencer-99.json", - "referenceNumber": 288, - "name": "Spencer License 99", - "licenseId": "Spencer-99", + "detailsUrl": "https://spdx.org/licenses/MIT-advertising.json", + "referenceNumber": 75, + "name": "Enlightenment License (e16)", + "licenseId": "MIT-advertising", "seeAlso": [ - "http://www.opensource.apple.com/source/tcl/tcl-5/tcl/generic/regfronts.c" + "https://fedoraproject.org/wiki/Licensing/MIT_With_Advertising" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/PolyForm-Noncommercial-1.0.0.html", + "reference": "https://spdx.org/licenses/MIT-CMU.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/PolyForm-Noncommercial-1.0.0.json", - "referenceNumber": 289, - "name": "PolyForm Noncommercial License 1.0.0", - "licenseId": "PolyForm-Noncommercial-1.0.0", + "detailsUrl": "https://spdx.org/licenses/MIT-CMU.json", + "referenceNumber": 164, + "name": "CMU License", + "licenseId": "MIT-CMU", "seeAlso": [ - "https://polyformproject.org/licenses/noncommercial/1.0.0" + "https://fedoraproject.org/wiki/Licensing:MIT?rd\u003dLicensing/MIT#CMU_Style", + "https://github.com/python-pillow/Pillow/blob/fffb426092c8db24a5f4b6df243a8a3c01fb63cd/LICENSE" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Abstyles.html", + "reference": "https://spdx.org/licenses/MIT-enna.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Abstyles.json", - "referenceNumber": 290, - "name": "Abstyles License", - "licenseId": "Abstyles", + "detailsUrl": "https://spdx.org/licenses/MIT-enna.json", + "referenceNumber": 121, + "name": "enna License", + "licenseId": "MIT-enna", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Abstyles" + "https://fedoraproject.org/wiki/Licensing/MIT#enna" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GFDL-1.3-or-later.html", + "reference": "https://spdx.org/licenses/MIT-feh.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.3-or-later.json", - "referenceNumber": 291, - "name": "GNU Free Documentation License v1.3 or later", - "licenseId": "GFDL-1.3-or-later", + "detailsUrl": "https://spdx.org/licenses/MIT-feh.json", + "referenceNumber": 145, + "name": "feh License", + "licenseId": "MIT-feh", "seeAlso": [ - "https://www.gnu.org/licenses/fdl-1.3.txt" + "https://fedoraproject.org/wiki/Licensing/MIT#feh" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/NRL.html", + "reference": "https://spdx.org/licenses/MIT-Modern-Variant.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NRL.json", - "referenceNumber": 292, - "name": "NRL License", - "licenseId": "NRL", + "detailsUrl": "https://spdx.org/licenses/MIT-Modern-Variant.json", + "referenceNumber": 12, + "name": "MIT License Modern Variant", + "licenseId": "MIT-Modern-Variant", "seeAlso": [ - "http://web.mit.edu/network/isakmp/nrllicense.html" + "https://fedoraproject.org/wiki/Licensing:MIT#Modern_Variants", + "https://ptolemy.berkeley.edu/copyright.htm", + "https://pirlwww.lpl.arizona.edu/resources/guide/software/PerlTk/Tixlic.html" + ], + "isOsiApproved": true + }, + { + "reference": "https://spdx.org/licenses/MIT-open-group.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/MIT-open-group.json", + "referenceNumber": 276, + "name": "MIT Open Group variant", + "licenseId": "MIT-open-group", + "seeAlso": [ + "https://gitlab.freedesktop.org/xorg/app/iceauth/-/blob/master/COPYING", + "https://gitlab.freedesktop.org/xorg/app/xvinfo/-/blob/master/COPYING", + "https://gitlab.freedesktop.org/xorg/app/xsetroot/-/blob/master/COPYING", + "https://gitlab.freedesktop.org/xorg/app/xauth/-/blob/master/COPYING" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Libpng.html", + "reference": "https://spdx.org/licenses/MIT-Wu.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Libpng.json", - "referenceNumber": 293, - "name": "libpng License", - "licenseId": "Libpng", + "detailsUrl": "https://spdx.org/licenses/MIT-Wu.json", + "referenceNumber": 415, + "name": "MIT Tom Wu Variant", + "licenseId": "MIT-Wu", "seeAlso": [ - "http://www.libpng.org/pub/png/src/libpng-LICENSE.txt" + "https://github.com/chromium/octane/blob/master/crypto.js" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/NAIST-2003.html", + "reference": "https://spdx.org/licenses/MITNFA.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NAIST-2003.json", - "referenceNumber": 294, - "name": "Nara Institute of Science and Technology License (2003)", - "licenseId": "NAIST-2003", + "detailsUrl": "https://spdx.org/licenses/MITNFA.json", + "referenceNumber": 73, + "name": "MIT +no-false-attribs license", + "licenseId": "MITNFA", "seeAlso": [ - "https://enterprise.dejacode.com/licenses/public/naist-2003/#license-text", - "https://github.com/nodejs/node/blob/4a19cc8947b1bba2b2d27816ec3d0edf9b28e503/LICENSE#L343" + "https://fedoraproject.org/wiki/Licensing/MITNFA" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/XFree86-1.1.html", + "reference": "https://spdx.org/licenses/Motosoto.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/XFree86-1.1.json", - "referenceNumber": 295, - "name": "XFree86 License 1.1", - "licenseId": "XFree86-1.1", + "detailsUrl": "https://spdx.org/licenses/Motosoto.json", + "referenceNumber": 267, + "name": "Motosoto License", + "licenseId": "Motosoto", "seeAlso": [ - "http://www.xfree86.org/current/LICENSE4.html" + "https://opensource.org/licenses/Motosoto" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/OFL-1.1-RFN.html", + "reference": "https://spdx.org/licenses/mpi-permissive.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OFL-1.1-RFN.json", - "referenceNumber": 296, - "name": "SIL Open Font License 1.1 with Reserved Font Name", - "licenseId": "OFL-1.1-RFN", + "detailsUrl": "https://spdx.org/licenses/mpi-permissive.json", + "referenceNumber": 235, + "name": "mpi Permissive License", + "licenseId": "mpi-permissive", "seeAlso": [ - "http://scripts.sil.org/cms/scripts/page.php?item_id\u003dOFL_web", - "https://opensource.org/licenses/OFL-1.1" + "https://sources.debian.org/src/openmpi/4.1.0-10/ompi/debuggers/msgq_interface.h/?hl\u003d19#L19" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-3.0-US.html", + "reference": "https://spdx.org/licenses/mpich2.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-3.0-US.json", - "referenceNumber": 297, - "name": "Creative Commons Attribution 3.0 United States", - "licenseId": "CC-BY-3.0-US", + "detailsUrl": "https://spdx.org/licenses/mpich2.json", + "referenceNumber": 246, + "name": "mpich2 License", + "licenseId": "mpich2", "seeAlso": [ - "https://creativecommons.org/licenses/by/3.0/us/legalcode" + "https://fedoraproject.org/wiki/Licensing/MIT" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/EFL-2.0.html", + "reference": "https://spdx.org/licenses/MPL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/EFL-2.0.json", - "referenceNumber": 298, - "name": "Eiffel Forum License v2.0", - "licenseId": "EFL-2.0", + "detailsUrl": "https://spdx.org/licenses/MPL-1.0.json", + "referenceNumber": 79, + "name": "Mozilla Public License 1.0", + "licenseId": "MPL-1.0", "seeAlso": [ - "http://www.eiffel-nice.org/license/eiffel-forum-license-2.html", - "https://opensource.org/licenses/EFL-2.0" + "http://www.mozilla.org/MPL/MPL-1.0.html", + "https://opensource.org/licenses/MPL-1.0" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/LPPL-1.0.html", + "reference": "https://spdx.org/licenses/MPL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LPPL-1.0.json", - "referenceNumber": 299, - "name": "LaTeX Project Public License v1.0", - "licenseId": "LPPL-1.0", + "detailsUrl": "https://spdx.org/licenses/MPL-1.1.json", + "referenceNumber": 10, + "name": "Mozilla Public License 1.1", + "licenseId": "MPL-1.1", "seeAlso": [ - "http://www.latex-project.org/lppl/lppl-1-0.txt" + "http://www.mozilla.org/MPL/MPL-1.1.html", + "https://opensource.org/licenses/MPL-1.1" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/IPL-1.0.html", + "reference": "https://spdx.org/licenses/MPL-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/IPL-1.0.json", - "referenceNumber": 300, - "name": "IBM Public License v1.0", - "licenseId": "IPL-1.0", + "detailsUrl": "https://spdx.org/licenses/MPL-2.0.json", + "referenceNumber": 438, + "name": "Mozilla Public License 2.0", + "licenseId": "MPL-2.0", "seeAlso": [ - "https://opensource.org/licenses/IPL-1.0" + "https://www.mozilla.org/MPL/2.0/", + "https://opensource.org/licenses/MPL-2.0" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/ANTLR-PD.html", + "reference": "https://spdx.org/licenses/MPL-2.0-no-copyleft-exception.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/ANTLR-PD.json", - "referenceNumber": 301, - "name": "ANTLR Software Rights Notice", - "licenseId": "ANTLR-PD", + "detailsUrl": "https://spdx.org/licenses/MPL-2.0-no-copyleft-exception.json", + "referenceNumber": 210, + "name": "Mozilla Public License 2.0 (no copyleft exception)", + "licenseId": "MPL-2.0-no-copyleft-exception", "seeAlso": [ - "http://www.antlr2.org/license.html" + "https://www.mozilla.org/MPL/2.0/", + "https://opensource.org/licenses/MPL-2.0" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/PolyForm-Small-Business-1.0.0.html", + "reference": "https://spdx.org/licenses/mplus.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/PolyForm-Small-Business-1.0.0.json", - "referenceNumber": 302, - "name": "PolyForm Small Business License 1.0.0", - "licenseId": "PolyForm-Small-Business-1.0.0", + "detailsUrl": "https://spdx.org/licenses/mplus.json", + "referenceNumber": 336, + "name": "mplus Font License", + "licenseId": "mplus", "seeAlso": [ - "https://polyformproject.org/licenses/small-business/1.0.0" + "https://fedoraproject.org/wiki/Licensing:Mplus?rd\u003dLicensing/mplus" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GPL-1.0-only.html", + "reference": "https://spdx.org/licenses/MS-LPL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GPL-1.0-only.json", - "referenceNumber": 303, - "name": "GNU General Public License v1.0 only", - "licenseId": "GPL-1.0-only", + "detailsUrl": "https://spdx.org/licenses/MS-LPL.json", + "referenceNumber": 406, + "name": "Microsoft Limited Public License", + "licenseId": "MS-LPL", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/gpl-1.0-standalone.html" + "https://www.openhub.net/licenses/mslpl", + "https://github.com/gabegundy/atlserver/blob/master/License.txt", + "https://en.wikipedia.org/wiki/Shared_Source_Initiative#Microsoft_Limited_Public_License_(Ms-LPL)" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CAL-1.0-Combined-Work-Exception.html", + "reference": "https://spdx.org/licenses/MS-PL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CAL-1.0-Combined-Work-Exception.json", - "referenceNumber": 304, - "name": "Cryptographic Autonomy License 1.0 (Combined Work Exception)", - "licenseId": "CAL-1.0-Combined-Work-Exception", + "detailsUrl": "https://spdx.org/licenses/MS-PL.json", + "referenceNumber": 509, + "name": "Microsoft Public License", + "licenseId": "MS-PL", "seeAlso": [ - "http://cryptographicautonomylicense.com/license-text.html", - "https://opensource.org/licenses/CAL-1.0" + "http://www.microsoft.com/opensource/licenses.mspx", + "https://opensource.org/licenses/MS-PL" ], - "isOsiApproved": true + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/FreeImage.html", + "reference": "https://spdx.org/licenses/MS-RL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/FreeImage.json", - "referenceNumber": 305, - "name": "FreeImage Public License v1.0", - "licenseId": "FreeImage", + "detailsUrl": "https://spdx.org/licenses/MS-RL.json", + "referenceNumber": 18, + "name": "Microsoft Reciprocal License", + "licenseId": "MS-RL", "seeAlso": [ - "http://freeimage.sourceforge.net/freeimage-license.txt" + "http://www.microsoft.com/opensource/licenses.mspx", + "https://opensource.org/licenses/MS-RL" + ], + "isOsiApproved": true, + "isFsfLibre": true + }, + { + "reference": "https://spdx.org/licenses/MTLL.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/MTLL.json", + "referenceNumber": 229, + "name": "Matrix Template Library License", + "licenseId": "MTLL", + "seeAlso": [ + "https://fedoraproject.org/wiki/Licensing/Matrix_Template_Library_License" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GPL-2.0-with-GCC-exception.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GPL-2.0-with-GCC-exception.json", - "referenceNumber": 306, - "name": "GNU General Public License v2.0 w/GCC Runtime Library exception", - "licenseId": "GPL-2.0-with-GCC-exception", + "reference": "https://spdx.org/licenses/MulanPSL-1.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/MulanPSL-1.0.json", + "referenceNumber": 312, + "name": "Mulan Permissive Software License, Version 1", + "licenseId": "MulanPSL-1.0", "seeAlso": [ - "https://gcc.gnu.org/git/?p\u003dgcc.git;a\u003dblob;f\u003dgcc/libgcc1.c;h\u003d762f5143fc6eed57b6797c82710f3538aa52b40b;hb\u003dcb143a3ce4fb417c68f5fa2691a1b1b1053dfba9#l10" + "https://license.coscl.org.cn/MulanPSL/", + "https://github.com/yuwenlong/longphp/blob/25dfb70cc2a466dc4bb55ba30901cbce08d164b5/LICENSE" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BSD-2-Clause-FreeBSD.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/BSD-2-Clause-FreeBSD.json", - "referenceNumber": 307, - "name": "BSD 2-Clause FreeBSD License", - "licenseId": "BSD-2-Clause-FreeBSD", + "reference": "https://spdx.org/licenses/MulanPSL-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/MulanPSL-2.0.json", + "referenceNumber": 74, + "name": "Mulan Permissive Software License, Version 2", + "licenseId": "MulanPSL-2.0", "seeAlso": [ - "http://www.freebsd.org/copyright/freebsd-license.html" + "https://license.coscl.org.cn/MulanPSL2/" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/OLDAP-2.2.1.html", + "reference": "https://spdx.org/licenses/Multics.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OLDAP-2.2.1.json", - "referenceNumber": 308, - "name": "Open LDAP Public License v2.2.1", - "licenseId": "OLDAP-2.2.1", + "detailsUrl": "https://spdx.org/licenses/Multics.json", + "referenceNumber": 236, + "name": "Multics License", + "licenseId": "Multics", "seeAlso": [ - "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d4bc786f34b50aa301be6f5600f58a980070f481e" + "https://opensource.org/licenses/Multics" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/GFDL-1.2-no-invariants-or-later.html", + "reference": "https://spdx.org/licenses/Mup.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.2-no-invariants-or-later.json", - "referenceNumber": 309, - "name": "GNU Free Documentation License v1.2 or later - no invariants", - "licenseId": "GFDL-1.2-no-invariants-or-later", + "detailsUrl": "https://spdx.org/licenses/Mup.json", + "referenceNumber": 98, + "name": "Mup License", + "licenseId": "Mup", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" + "https://fedoraproject.org/wiki/Licensing/Mup" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GFDL-1.2-no-invariants-only.html", + "reference": "https://spdx.org/licenses/NAIST-2003.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.2-no-invariants-only.json", - "referenceNumber": 310, - "name": "GNU Free Documentation License v1.2 only - no invariants", - "licenseId": "GFDL-1.2-no-invariants-only", + "detailsUrl": "https://spdx.org/licenses/NAIST-2003.json", + "referenceNumber": 408, + "name": "Nara Institute of Science and Technology License (2003)", + "licenseId": "NAIST-2003", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" + "https://enterprise.dejacode.com/licenses/public/naist-2003/#license-text", + "https://github.com/nodejs/node/blob/4a19cc8947b1bba2b2d27816ec3d0edf9b28e503/LICENSE#L343" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Afmparse.html", + "reference": "https://spdx.org/licenses/NASA-1.3.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Afmparse.json", - "referenceNumber": 311, - "name": "Afmparse License", - "licenseId": "Afmparse", + "detailsUrl": "https://spdx.org/licenses/NASA-1.3.json", + "referenceNumber": 139, + "name": "NASA Open Source Agreement 1.3", + "licenseId": "NASA-1.3", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Afmparse" + "http://ti.arc.nasa.gov/opensource/nosa/", + "https://opensource.org/licenses/NASA-1.3" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/OLDAP-2.2.2.html", + "reference": "https://spdx.org/licenses/Naumen.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OLDAP-2.2.2.json", - "referenceNumber": 312, - "name": "Open LDAP Public License 2.2.2", - "licenseId": "OLDAP-2.2.2", + "detailsUrl": "https://spdx.org/licenses/Naumen.json", + "referenceNumber": 371, + "name": "Naumen Public License", + "licenseId": "Naumen", "seeAlso": [ - "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003ddf2cc1e21eb7c160695f5b7cffd6296c151ba188" + "https://opensource.org/licenses/Naumen" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/Parity-6.0.0.html", + "reference": "https://spdx.org/licenses/NBPL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Parity-6.0.0.json", - "referenceNumber": 313, - "name": "The Parity Public License 6.0.0", - "licenseId": "Parity-6.0.0", + "detailsUrl": "https://spdx.org/licenses/NBPL-1.0.json", + "referenceNumber": 283, + "name": "Net Boolean Public License v1", + "licenseId": "NBPL-1.0", "seeAlso": [ - "https://paritylicense.com/versions/6.0.0.html" + "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d37b4b3f6cc4bf34e1d3dec61e69914b9819d8894" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/LGPL-2.1-only.html", + "reference": "https://spdx.org/licenses/NCGL-UK-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LGPL-2.1-only.json", - "referenceNumber": 314, - "name": "GNU Lesser General Public License v2.1 only", - "licenseId": "LGPL-2.1-only", + "detailsUrl": "https://spdx.org/licenses/NCGL-UK-2.0.json", + "referenceNumber": 49, + "name": "Non-Commercial Government Licence", + "licenseId": "NCGL-UK-2.0", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", - "https://opensource.org/licenses/LGPL-2.1" + "http://www.nationalarchives.gov.uk/doc/non-commercial-government-licence/version/2/" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/MPL-1.1.html", + "reference": "https://spdx.org/licenses/NCSA.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MPL-1.1.json", - "referenceNumber": 315, - "name": "Mozilla Public License 1.1", - "licenseId": "MPL-1.1", + "detailsUrl": "https://spdx.org/licenses/NCSA.json", + "referenceNumber": 207, + "name": "University of Illinois/NCSA Open Source License", + "licenseId": "NCSA", "seeAlso": [ - "http://www.mozilla.org/MPL/MPL-1.1.html", - "https://opensource.org/licenses/MPL-1.1" + "http://otm.illinois.edu/uiuc_openSource", + "https://opensource.org/licenses/NCSA" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/bzip2-1.0.5.html", + "reference": "https://spdx.org/licenses/Net-SNMP.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/bzip2-1.0.5.json", - "referenceNumber": 316, - "name": "bzip2 and libbzip2 License v1.0.5", - "licenseId": "bzip2-1.0.5", + "detailsUrl": "https://spdx.org/licenses/Net-SNMP.json", + "referenceNumber": 347, + "name": "Net-SNMP License", + "licenseId": "Net-SNMP", "seeAlso": [ - "https://sourceware.org/bzip2/1.0.5/bzip2-manual-1.0.5.html", - "http://bzip.org/1.0.5/bzip2-manual-1.0.5.html" + "http://net-snmp.sourceforge.net/about/license.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Cube.html", + "reference": "https://spdx.org/licenses/NetCDF.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Cube.json", - "referenceNumber": 317, - "name": "Cube License", - "licenseId": "Cube", + "detailsUrl": "https://spdx.org/licenses/NetCDF.json", + "referenceNumber": 348, + "name": "NetCDF license", + "licenseId": "NetCDF", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Cube" + "http://www.unidata.ucar.edu/software/netcdf/copyright.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/SISSL.html", + "reference": "https://spdx.org/licenses/Newsletr.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SISSL.json", - "referenceNumber": 318, - "name": "Sun Industry Standards Source License v1.1", - "licenseId": "SISSL", + "detailsUrl": "https://spdx.org/licenses/Newsletr.json", + "referenceNumber": 221, + "name": "Newsletr License", + "licenseId": "Newsletr", "seeAlso": [ - "http://www.openoffice.org/licenses/sissl_license.html", - "https://opensource.org/licenses/SISSL" + "https://fedoraproject.org/wiki/Licensing/Newsletr" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/PostgreSQL.html", + "reference": "https://spdx.org/licenses/NGPL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/PostgreSQL.json", - "referenceNumber": 319, - "name": "PostgreSQL License", - "licenseId": "PostgreSQL", + "detailsUrl": "https://spdx.org/licenses/NGPL.json", + "referenceNumber": 311, + "name": "Nethack General Public License", + "licenseId": "NGPL", "seeAlso": [ - "http://www.postgresql.org/about/licence", - "https://opensource.org/licenses/PostgreSQL" + "https://opensource.org/licenses/NGPL" ], "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/CERN-OHL-P-2.0.html", + "reference": "https://spdx.org/licenses/NICTA-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CERN-OHL-P-2.0.json", - "referenceNumber": 320, - "name": "CERN Open Hardware Licence Version 2 - Permissive", - "licenseId": "CERN-OHL-P-2.0", + "detailsUrl": "https://spdx.org/licenses/NICTA-1.0.json", + "referenceNumber": 107, + "name": "NICTA Public Software License, Version 1.0", + "licenseId": "NICTA-1.0", "seeAlso": [ - "https://www.ohwr.org/project/cernohl/wikis/Documents/CERN-OHL-version-2" + "https://opensource.apple.com/source/mDNSResponder/mDNSResponder-320.10/mDNSPosix/nss_ReadMe.txt" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OSL-3.0.html", + "reference": "https://spdx.org/licenses/NIST-PD.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OSL-3.0.json", - "referenceNumber": 321, - "name": "Open Software License 3.0", - "licenseId": "OSL-3.0", + "detailsUrl": "https://spdx.org/licenses/NIST-PD.json", + "referenceNumber": 182, + "name": "NIST Public Domain Notice", + "licenseId": "NIST-PD", "seeAlso": [ - "https://web.archive.org/web/20120101081418/http://rosenlaw.com:80/OSL3.0.htm", - "https://opensource.org/licenses/OSL-3.0" + "https://github.com/tcheneau/simpleRPL/blob/e645e69e38dd4e3ccfeceb2db8cba05b7c2e0cd3/LICENSE.txt", + "https://github.com/tcheneau/Routing/blob/f09f46fcfe636107f22f2c98348188a65a135d98/README.md" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/LPPL-1.2.html", + "reference": "https://spdx.org/licenses/NIST-PD-fallback.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LPPL-1.2.json", - "referenceNumber": 322, - "name": "LaTeX Project Public License v1.2", - "licenseId": "LPPL-1.2", + "detailsUrl": "https://spdx.org/licenses/NIST-PD-fallback.json", + "referenceNumber": 357, + "name": "NIST Public Domain Notice with license fallback", + "licenseId": "NIST-PD-fallback", "seeAlso": [ - "http://www.latex-project.org/lppl/lppl-1-2.txt" + "https://github.com/usnistgov/jsip/blob/59700e6926cbe96c5cdae897d9a7d2656b42abe3/LICENSE", + "https://github.com/usnistgov/fipy/blob/86aaa5c2ba2c6f1be19593c5986071cf6568cc34/LICENSE.rst" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/xpp.html", + "reference": "https://spdx.org/licenses/NLOD-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/xpp.json", - "referenceNumber": 323, - "name": "XPP License", - "licenseId": "xpp", + "detailsUrl": "https://spdx.org/licenses/NLOD-1.0.json", + "referenceNumber": 496, + "name": "Norwegian Licence for Open Government Data (NLOD) 1.0", + "licenseId": "NLOD-1.0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/xpp" + "http://data.norge.no/nlod/en/1.0" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OLDAP-2.8.html", + "reference": "https://spdx.org/licenses/NLOD-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OLDAP-2.8.json", - "referenceNumber": 324, - "name": "Open LDAP Public License v2.8", - "licenseId": "OLDAP-2.8", + "detailsUrl": "https://spdx.org/licenses/NLOD-2.0.json", + "referenceNumber": 341, + "name": "Norwegian Licence for Open Government Data (NLOD) 2.0", + "licenseId": "NLOD-2.0", "seeAlso": [ - "http://www.openldap.org/software/release/license.html" + "http://data.norge.no/nlod/en/2.0" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/AML.html", + "reference": "https://spdx.org/licenses/NLPL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/AML.json", - "referenceNumber": 325, - "name": "Apple MIT License", - "licenseId": "AML", + "detailsUrl": "https://spdx.org/licenses/NLPL.json", + "referenceNumber": 138, + "name": "No Limit Public License", + "licenseId": "NLPL", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Apple_MIT_License" + "https://fedoraproject.org/wiki/Licensing/NLPL" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Motosoto.html", + "reference": "https://spdx.org/licenses/Nokia.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Motosoto.json", - "referenceNumber": 326, - "name": "Motosoto License", - "licenseId": "Motosoto", + "detailsUrl": "https://spdx.org/licenses/Nokia.json", + "referenceNumber": 441, + "name": "Nokia Open Source License", + "licenseId": "Nokia", "seeAlso": [ - "https://opensource.org/licenses/Motosoto" + "https://opensource.org/licenses/nokia" ], - "isOsiApproved": true + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Vim.html", + "reference": "https://spdx.org/licenses/NOSL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Vim.json", - "referenceNumber": 327, - "name": "Vim License", - "licenseId": "Vim", + "detailsUrl": "https://spdx.org/licenses/NOSL.json", + "referenceNumber": 193, + "name": "Netizen Open Source License", + "licenseId": "NOSL", "seeAlso": [ - "http://vimdoc.sourceforge.net/htmldoc/uganda.html" + "http://bits.netizen.com.au/licenses/NOSL/nosl.txt" ], "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/TCP-wrappers.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/TCP-wrappers.json", - "referenceNumber": 328, - "name": "TCP Wrappers License", - "licenseId": "TCP-wrappers", - "seeAlso": [ - "http://rc.quest.com/topics/openssh/license.php#tcpwrappers" - ], - "isOsiApproved": false - }, - { - "reference": "https://spdx.org/licenses/Sendmail-8.23.html", + "reference": "https://spdx.org/licenses/Noweb.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Sendmail-8.23.json", - "referenceNumber": 329, - "name": "Sendmail License 8.23", - "licenseId": "Sendmail-8.23", + "detailsUrl": "https://spdx.org/licenses/Noweb.json", + "referenceNumber": 335, + "name": "Noweb License", + "licenseId": "Noweb", "seeAlso": [ - "https://www.proofpoint.com/sites/default/files/sendmail-license.pdf", - "https://web.archive.org/web/20181003101040/https://www.proofpoint.com/sites/default/files/sendmail-license.pdf" + "https://fedoraproject.org/wiki/Licensing/Noweb" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Ruby.html", + "reference": "https://spdx.org/licenses/NPL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Ruby.json", - "referenceNumber": 330, - "name": "Ruby License", - "licenseId": "Ruby", + "detailsUrl": "https://spdx.org/licenses/NPL-1.0.json", + "referenceNumber": 328, + "name": "Netscape Public License v1.0", + "licenseId": "NPL-1.0", "seeAlso": [ - "http://www.ruby-lang.org/en/LICENSE.txt" + "http://www.mozilla.org/MPL/NPL/1.0/" ], "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/GFDL-1.1-invariants-or-later.html", + "reference": "https://spdx.org/licenses/NPL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.1-invariants-or-later.json", - "referenceNumber": 331, - "name": "GNU Free Documentation License v1.1 or later - invariants", - "licenseId": "GFDL-1.1-invariants-or-later", + "detailsUrl": "https://spdx.org/licenses/NPL-1.1.json", + "referenceNumber": 52, + "name": "Netscape Public License v1.1", + "licenseId": "NPL-1.1", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" + "http://www.mozilla.org/MPL/NPL/1.1/" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CC-BY-2.5-AU.html", + "reference": "https://spdx.org/licenses/NPOSL-3.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-2.5-AU.json", - "referenceNumber": 332, - "name": "Creative Commons Attribution 2.5 Australia", - "licenseId": "CC-BY-2.5-AU", + "detailsUrl": "https://spdx.org/licenses/NPOSL-3.0.json", + "referenceNumber": 286, + "name": "Non-Profit Open Software License 3.0", + "licenseId": "NPOSL-3.0", "seeAlso": [ - "https://creativecommons.org/licenses/by/2.5/au/legalcode" + "https://opensource.org/licenses/NOSL3.0" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/NetCDF.html", + "reference": "https://spdx.org/licenses/NRL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NetCDF.json", - "referenceNumber": 333, - "name": "NetCDF license", - "licenseId": "NetCDF", + "detailsUrl": "https://spdx.org/licenses/NRL.json", + "referenceNumber": 454, + "name": "NRL License", + "licenseId": "NRL", "seeAlso": [ - "http://www.unidata.ucar.edu/software/netcdf/copyright.html" + "http://web.mit.edu/network/isakmp/nrllicense.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BSD-1-Clause.html", + "reference": "https://spdx.org/licenses/NTP.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-1-Clause.json", - "referenceNumber": 334, - "name": "BSD 1-Clause License", - "licenseId": "BSD-1-Clause", + "detailsUrl": "https://spdx.org/licenses/NTP.json", + "referenceNumber": 255, + "name": "NTP License", + "licenseId": "NTP", "seeAlso": [ - "https://svnweb.freebsd.org/base/head/include/ifaddrs.h?revision\u003d326823" + "https://opensource.org/licenses/NTP" ], "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/Leptonica.html", + "reference": "https://spdx.org/licenses/NTP-0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Leptonica.json", - "referenceNumber": 335, - "name": "Leptonica License", - "licenseId": "Leptonica", + "detailsUrl": "https://spdx.org/licenses/NTP-0.json", + "referenceNumber": 379, + "name": "NTP No Attribution", + "licenseId": "NTP-0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Leptonica" + "https://github.com/tytso/e2fsprogs/blob/master/lib/et/et_name.c" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/AGPL-3.0-or-later.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/AGPL-3.0-or-later.json", - "referenceNumber": 336, - "name": "GNU Affero General Public License v3.0 or later", - "licenseId": "AGPL-3.0-or-later", + "reference": "https://spdx.org/licenses/Nunit.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/Nunit.json", + "referenceNumber": 244, + "name": "Nunit License", + "licenseId": "Nunit", "seeAlso": [ - "https://www.gnu.org/licenses/agpl.txt", - "https://opensource.org/licenses/AGPL-3.0" + "https://fedoraproject.org/wiki/Licensing/Nunit" ], - "isOsiApproved": true, + "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Beerware.html", + "reference": "https://spdx.org/licenses/O-UDA-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Beerware.json", - "referenceNumber": 337, - "name": "Beerware License", - "licenseId": "Beerware", + "detailsUrl": "https://spdx.org/licenses/O-UDA-1.0.json", + "referenceNumber": 4, + "name": "Open Use of Data Agreement v1.0", + "licenseId": "O-UDA-1.0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Beerware", - "https://people.freebsd.org/~phk/" + "https://github.com/microsoft/Open-Use-of-Data-Agreement/blob/v1.0/O-UDA-1.0.md", + "https://cdla.dev/open-use-of-data-agreement-v1-0/" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/EUPL-1.2.html", + "reference": "https://spdx.org/licenses/OCCT-PL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/EUPL-1.2.json", - "referenceNumber": 338, - "name": "European Union Public License 1.2", - "licenseId": "EUPL-1.2", + "detailsUrl": "https://spdx.org/licenses/OCCT-PL.json", + "referenceNumber": 112, + "name": "Open CASCADE Technology Public License", + "licenseId": "OCCT-PL", "seeAlso": [ - "https://joinup.ec.europa.eu/page/eupl-text-11-12", - "https://joinup.ec.europa.eu/sites/default/files/custom-page/attachment/eupl_v1.2_en.pdf", - "https://joinup.ec.europa.eu/sites/default/files/custom-page/attachment/2020-03/EUPL-1.2%20EN.txt", - "https://joinup.ec.europa.eu/sites/default/files/inline-files/EUPL%20v1_2%20EN(1).txt", - "http://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri\u003dCELEX:32017D0863", - "https://opensource.org/licenses/EUPL-1.2" + "http://www.opencascade.com/content/occt-public-license" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OSET-PL-2.1.html", + "reference": "https://spdx.org/licenses/OCLC-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OSET-PL-2.1.json", - "referenceNumber": 339, - "name": "OSET Public License version 2.1", - "licenseId": "OSET-PL-2.1", + "detailsUrl": "https://spdx.org/licenses/OCLC-2.0.json", + "referenceNumber": 222, + "name": "OCLC Research Public License 2.0", + "licenseId": "OCLC-2.0", "seeAlso": [ - "http://www.osetfoundation.org/public-license", - "https://opensource.org/licenses/OPL-2.1" + "http://www.oclc.org/research/activities/software/license/v2final.htm", + "https://opensource.org/licenses/OCLC-2.0" ], "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/CC-BY-1.0.html", + "reference": "https://spdx.org/licenses/ODbL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-1.0.json", - "referenceNumber": 340, - "name": "Creative Commons Attribution 1.0 Generic", - "licenseId": "CC-BY-1.0", + "detailsUrl": "https://spdx.org/licenses/ODbL-1.0.json", + "referenceNumber": 365, + "name": "Open Data Commons Open Database License v1.0", + "licenseId": "ODbL-1.0", "seeAlso": [ - "https://creativecommons.org/licenses/by/1.0/legalcode" + "http://www.opendatacommons.org/licenses/odbl/1.0/", + "https://opendatacommons.org/licenses/odbl/1-0/" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/SISSL-1.2.html", + "reference": "https://spdx.org/licenses/ODC-By-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SISSL-1.2.json", - "referenceNumber": 341, - "name": "Sun Industry Standards Source License v1.2", - "licenseId": "SISSL-1.2", + "detailsUrl": "https://spdx.org/licenses/ODC-By-1.0.json", + "referenceNumber": 300, + "name": "Open Data Commons Attribution License v1.0", + "licenseId": "ODC-By-1.0", "seeAlso": [ - "http://gridscheduler.sourceforge.net/Gridengine_SISSL_license.html" + "https://opendatacommons.org/licenses/by/1.0/" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-3.0.html", + "reference": "https://spdx.org/licenses/OFL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-3.0.json", - "referenceNumber": 342, - "name": "Creative Commons Attribution Non Commercial 3.0 Unported", - "licenseId": "CC-BY-NC-3.0", + "detailsUrl": "https://spdx.org/licenses/OFL-1.0.json", + "referenceNumber": 288, + "name": "SIL Open Font License 1.0", + "licenseId": "OFL-1.0", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc/3.0/legalcode" + "http://scripts.sil.org/cms/scripts/page.php?item_id\u003dOFL10_web" ], "isOsiApproved": false, - "isFsfLibre": false + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/GPL-2.0+.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GPL-2.0+.json", - "referenceNumber": 343, - "name": "GNU General Public License v2.0 or later", - "licenseId": "GPL-2.0+", + "reference": "https://spdx.org/licenses/OFL-1.0-no-RFN.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/OFL-1.0-no-RFN.json", + "referenceNumber": 160, + "name": "SIL Open Font License 1.0 with no Reserved Font Name", + "licenseId": "OFL-1.0-no-RFN", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", - "https://opensource.org/licenses/GPL-2.0" + "http://scripts.sil.org/cms/scripts/page.php?item_id\u003dOFL10_web" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OLDAP-1.4.html", + "reference": "https://spdx.org/licenses/OFL-1.0-RFN.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OLDAP-1.4.json", - "referenceNumber": 344, - "name": "Open LDAP Public License v1.4", - "licenseId": "OLDAP-1.4", + "detailsUrl": "https://spdx.org/licenses/OFL-1.0-RFN.json", + "referenceNumber": 151, + "name": "SIL Open Font License 1.0 with Reserved Font Name", + "licenseId": "OFL-1.0-RFN", "seeAlso": [ - "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003dc9f95c2f3f2ffb5e0ae55fe7388af75547660941" + "http://scripts.sil.org/cms/scripts/page.php?item_id\u003dOFL10_web" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/X11.html", + "reference": "https://spdx.org/licenses/OFL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/X11.json", - "referenceNumber": 345, - "name": "X11 License", - "licenseId": "X11", + "detailsUrl": "https://spdx.org/licenses/OFL-1.1.json", + "referenceNumber": 85, + "name": "SIL Open Font License 1.1", + "licenseId": "OFL-1.1", "seeAlso": [ - "http://www.xfree86.org/3.3.6/COPYRIGHT2.html#3" + "http://scripts.sil.org/cms/scripts/page.php?item_id\u003dOFL_web", + "https://opensource.org/licenses/OFL-1.1" ], - "isOsiApproved": false, + "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Intel.html", + "reference": "https://spdx.org/licenses/OFL-1.1-no-RFN.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Intel.json", - "referenceNumber": 346, - "name": "Intel Open Source License", - "licenseId": "Intel", + "detailsUrl": "https://spdx.org/licenses/OFL-1.1-no-RFN.json", + "referenceNumber": 477, + "name": "SIL Open Font License 1.1 with no Reserved Font Name", + "licenseId": "OFL-1.1-no-RFN", "seeAlso": [ - "https://opensource.org/licenses/Intel" + "http://scripts.sil.org/cms/scripts/page.php?item_id\u003dOFL_web", + "https://opensource.org/licenses/OFL-1.1" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/CDLA-Permissive-1.0.html", + "reference": "https://spdx.org/licenses/OFL-1.1-RFN.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CDLA-Permissive-1.0.json", - "referenceNumber": 347, - "name": "Community Data License Agreement Permissive 1.0", - "licenseId": "CDLA-Permissive-1.0", + "detailsUrl": "https://spdx.org/licenses/OFL-1.1-RFN.json", + "referenceNumber": 114, + "name": "SIL Open Font License 1.1 with Reserved Font Name", + "licenseId": "OFL-1.1-RFN", "seeAlso": [ - "https://cdla.io/permissive-1-0" + "http://scripts.sil.org/cms/scripts/page.php?item_id\u003dOFL_web", + "https://opensource.org/licenses/OFL-1.1" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/NIST-PD.html", + "reference": "https://spdx.org/licenses/OGC-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NIST-PD.json", - "referenceNumber": 348, - "name": "NIST Public Domain Notice", - "licenseId": "NIST-PD", + "detailsUrl": "https://spdx.org/licenses/OGC-1.0.json", + "referenceNumber": 410, + "name": "OGC Software License, Version 1.0", + "licenseId": "OGC-1.0", "seeAlso": [ - "https://github.com/tcheneau/simpleRPL/blob/e645e69e38dd4e3ccfeceb2db8cba05b7c2e0cd3/LICENSE.txt", - "https://github.com/tcheneau/Routing/blob/f09f46fcfe636107f22f2c98348188a65a135d98/README.md" + "https://www.ogc.org/ogc/software/1.0" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/LiLiQ-R-1.1.html", + "reference": "https://spdx.org/licenses/OGDL-Taiwan-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LiLiQ-R-1.1.json", - "referenceNumber": 349, - "name": "Licence Libre du Québec – Réciprocité version 1.1", - "licenseId": "LiLiQ-R-1.1", + "detailsUrl": "https://spdx.org/licenses/OGDL-Taiwan-1.0.json", + "referenceNumber": 110, + "name": "Taiwan Open Government Data License, version 1.0", + "licenseId": "OGDL-Taiwan-1.0", "seeAlso": [ - "https://www.forge.gouv.qc.ca/participez/licence-logicielle/licence-libre-du-quebec-liliq-en-francais/licence-libre-du-quebec-reciprocite-liliq-r-v1-1/", - "http://opensource.org/licenses/LiLiQ-R-1.1" + "https://data.gov.tw/license" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/SMLNJ.html", + "reference": "https://spdx.org/licenses/OGL-Canada-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SMLNJ.json", - "referenceNumber": 350, - "name": "Standard ML of New Jersey License", - "licenseId": "SMLNJ", + "detailsUrl": "https://spdx.org/licenses/OGL-Canada-2.0.json", + "referenceNumber": 99, + "name": "Open Government Licence - Canada", + "licenseId": "OGL-Canada-2.0", "seeAlso": [ - "https://www.smlnj.org/license.html" + "https://open.canada.ca/en/open-government-licence-canada" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BUSL-1.1.html", + "reference": "https://spdx.org/licenses/OGL-UK-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BUSL-1.1.json", - "referenceNumber": 351, - "name": "Business Source License 1.1", - "licenseId": "BUSL-1.1", + "detailsUrl": "https://spdx.org/licenses/OGL-UK-1.0.json", + "referenceNumber": 202, + "name": "Open Government Licence v1.0", + "licenseId": "OGL-UK-1.0", "seeAlso": [ - "https://mariadb.com/bsl11/" + "http://www.nationalarchives.gov.uk/doc/open-government-licence/version/1/" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CDDL-1.1.html", + "reference": "https://spdx.org/licenses/OGL-UK-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CDDL-1.1.json", - "referenceNumber": 352, - "name": "Common Development and Distribution License 1.1", - "licenseId": "CDDL-1.1", + "detailsUrl": "https://spdx.org/licenses/OGL-UK-2.0.json", + "referenceNumber": 381, + "name": "Open Government Licence v2.0", + "licenseId": "OGL-UK-2.0", "seeAlso": [ - "http://glassfish.java.net/public/CDDL+GPL_1_1.html", - "https://javaee.github.io/glassfish/LICENSE" + "http://www.nationalarchives.gov.uk/doc/open-government-licence/version/2/" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-SA-3.0.html", + "reference": "https://spdx.org/licenses/OGL-UK-3.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-3.0.json", - "referenceNumber": 353, - "name": "Creative Commons Attribution Share Alike 3.0 Unported", - "licenseId": "CC-BY-SA-3.0", + "detailsUrl": "https://spdx.org/licenses/OGL-UK-3.0.json", + "referenceNumber": 187, + "name": "Open Government Licence v3.0", + "licenseId": "OGL-UK-3.0", "seeAlso": [ - "https://creativecommons.org/licenses/by-sa/3.0/legalcode" + "http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-SA-2.1-JP.html", + "reference": "https://spdx.org/licenses/OGTSL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-2.1-JP.json", - "referenceNumber": 354, - "name": "Creative Commons Attribution Share Alike 2.1 Japan", - "licenseId": "CC-BY-SA-2.1-JP", + "detailsUrl": "https://spdx.org/licenses/OGTSL.json", + "referenceNumber": 89, + "name": "Open Group Test Suite License", + "licenseId": "OGTSL", "seeAlso": [ - "https://creativecommons.org/licenses/by-sa/2.1/jp/legalcode" + "http://www.opengroup.org/testing/downloads/The_Open_Group_TSL.txt", + "https://opensource.org/licenses/OGTSL" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/Wsuipa.html", + "reference": "https://spdx.org/licenses/OLDAP-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Wsuipa.json", - "referenceNumber": 355, - "name": "Wsuipa License", - "licenseId": "Wsuipa", + "detailsUrl": "https://spdx.org/licenses/OLDAP-1.1.json", + "referenceNumber": 486, + "name": "Open LDAP Public License v1.1", + "licenseId": "OLDAP-1.1", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Wsuipa" + "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d806557a5ad59804ef3a44d5abfbe91d706b0791f" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/MPL-2.0-no-copyleft-exception.html", + "reference": "https://spdx.org/licenses/OLDAP-1.2.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MPL-2.0-no-copyleft-exception.json", - "referenceNumber": 356, - "name": "Mozilla Public License 2.0 (no copyleft exception)", - "licenseId": "MPL-2.0-no-copyleft-exception", + "detailsUrl": "https://spdx.org/licenses/OLDAP-1.2.json", + "referenceNumber": 377, + "name": "Open LDAP Public License v1.2", + "licenseId": "OLDAP-1.2", "seeAlso": [ - "http://www.mozilla.org/MPL/2.0/", - "https://opensource.org/licenses/MPL-2.0" + "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d42b0383c50c299977b5893ee695cf4e486fb0dc7" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Unicode-DFS-2015.html", + "reference": "https://spdx.org/licenses/OLDAP-1.3.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Unicode-DFS-2015.json", - "referenceNumber": 357, - "name": "Unicode License Agreement - Data Files and Software (2015)", - "licenseId": "Unicode-DFS-2015", + "detailsUrl": "https://spdx.org/licenses/OLDAP-1.3.json", + "referenceNumber": 1, + "name": "Open LDAP Public License v1.3", + "licenseId": "OLDAP-1.3", "seeAlso": [ - "https://web.archive.org/web/20151224134844/http://unicode.org/copyright.html" + "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003de5f8117f0ce088d0bd7a8e18ddf37eaa40eb09b1" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GPL-2.0-with-classpath-exception.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GPL-2.0-with-classpath-exception.json", - "referenceNumber": 358, - "name": "GNU General Public License v2.0 w/Classpath exception", - "licenseId": "GPL-2.0-with-classpath-exception", + "reference": "https://spdx.org/licenses/OLDAP-1.4.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/OLDAP-1.4.json", + "referenceNumber": 445, + "name": "Open LDAP Public License v1.4", + "licenseId": "OLDAP-1.4", "seeAlso": [ - "https://www.gnu.org/software/classpath/license.html" + "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003dc9f95c2f3f2ffb5e0ae55fe7388af75547660941" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CNRI-Python-GPL-Compatible.html", + "reference": "https://spdx.org/licenses/OLDAP-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CNRI-Python-GPL-Compatible.json", - "referenceNumber": 359, - "name": "CNRI Python Open Source GPL Compatible License Agreement", - "licenseId": "CNRI-Python-GPL-Compatible", + "detailsUrl": "https://spdx.org/licenses/OLDAP-2.0.json", + "referenceNumber": 155, + "name": "Open LDAP Public License v2.0 (or possibly 2.0A and 2.0B)", + "licenseId": "OLDAP-2.0", "seeAlso": [ - "http://www.python.org/download/releases/1.6.1/download_win/" + "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003dcbf50f4e1185a21abd4c0a54d3f4341fe28f36ea" ], "isOsiApproved": false }, @@ -4531,7 +4934,7 @@ "reference": "https://spdx.org/licenses/OLDAP-2.0.1.html", "isDeprecatedLicenseId": false, "detailsUrl": "https://spdx.org/licenses/OLDAP-2.0.1.json", - "referenceNumber": 360, + "referenceNumber": 169, "name": "Open LDAP Public License v2.0.1", "licenseId": "OLDAP-2.0.1", "seeAlso": [ @@ -4540,1489 +4943,1481 @@ "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/psutils.html", + "reference": "https://spdx.org/licenses/OLDAP-2.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/psutils.json", - "referenceNumber": 361, - "name": "psutils License", - "licenseId": "psutils", + "detailsUrl": "https://spdx.org/licenses/OLDAP-2.1.json", + "referenceNumber": 248, + "name": "Open LDAP Public License v2.1", + "licenseId": "OLDAP-2.1", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/psutils" + "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003db0d176738e96a0d3b9f85cb51e140a86f21be715" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/eCos-2.0.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/eCos-2.0.json", - "referenceNumber": 362, - "name": "eCos license version 2.0", - "licenseId": "eCos-2.0", - "seeAlso": [ - "https://www.gnu.org/licenses/ecos-license.html" - ], - "isOsiApproved": false, - "isFsfLibre": true - }, - { - "reference": "https://spdx.org/licenses/CC-BY-2.0.html", + "reference": "https://spdx.org/licenses/OLDAP-2.2.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-2.0.json", - "referenceNumber": 363, - "name": "Creative Commons Attribution 2.0 Generic", - "licenseId": "CC-BY-2.0", + "detailsUrl": "https://spdx.org/licenses/OLDAP-2.2.json", + "referenceNumber": 499, + "name": "Open LDAP Public License v2.2", + "licenseId": "OLDAP-2.2", "seeAlso": [ - "https://creativecommons.org/licenses/by/2.0/legalcode" + "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d470b0c18ec67621c85881b2733057fecf4a1acc3" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Artistic-2.0.html", + "reference": "https://spdx.org/licenses/OLDAP-2.2.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Artistic-2.0.json", - "referenceNumber": 364, - "name": "Artistic License 2.0", - "licenseId": "Artistic-2.0", + "detailsUrl": "https://spdx.org/licenses/OLDAP-2.2.1.json", + "referenceNumber": 2, + "name": "Open LDAP Public License v2.2.1", + "licenseId": "OLDAP-2.2.1", "seeAlso": [ - "http://www.perlfoundation.org/artistic_license_2_0", - "https://www.perlfoundation.org/artistic-license-20.html", - "https://opensource.org/licenses/artistic-license-2.0" + "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d4bc786f34b50aa301be6f5600f58a980070f481e" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Artistic-1.0-cl8.html", + "reference": "https://spdx.org/licenses/OLDAP-2.2.2.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Artistic-1.0-cl8.json", - "referenceNumber": 365, - "name": "Artistic License 1.0 w/clause 8", - "licenseId": "Artistic-1.0-cl8", + "detailsUrl": "https://spdx.org/licenses/OLDAP-2.2.2.json", + "referenceNumber": 338, + "name": "Open LDAP Public License 2.2.2", + "licenseId": "OLDAP-2.2.2", "seeAlso": [ - "https://opensource.org/licenses/Artistic-1.0" + "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003ddf2cc1e21eb7c160695f5b7cffd6296c151ba188" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Aladdin.html", + "reference": "https://spdx.org/licenses/OLDAP-2.3.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Aladdin.json", - "referenceNumber": 366, - "name": "Aladdin Free Public License", - "licenseId": "Aladdin", + "detailsUrl": "https://spdx.org/licenses/OLDAP-2.3.json", + "referenceNumber": 103, + "name": "Open LDAP Public License v2.3", + "licenseId": "OLDAP-2.3", "seeAlso": [ - "http://pages.cs.wisc.edu/~ghost/doc/AFPL/6.01/Public.htm" + "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003dd32cf54a32d581ab475d23c810b0a7fbaf8d63c3" ], "isOsiApproved": false, - "isFsfLibre": false + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/LGPL-3.0-or-later.html", + "reference": "https://spdx.org/licenses/OLDAP-2.4.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LGPL-3.0-or-later.json", - "referenceNumber": 367, - "name": "GNU Lesser General Public License v3.0 or later", - "licenseId": "LGPL-3.0-or-later", + "detailsUrl": "https://spdx.org/licenses/OLDAP-2.4.json", + "referenceNumber": 382, + "name": "Open LDAP Public License v2.4", + "licenseId": "OLDAP-2.4", "seeAlso": [ - "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", - "https://opensource.org/licenses/LGPL-3.0" + "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003dcd1284c4a91a8a380d904eee68d1583f989ed386" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/SNIA.html", + "reference": "https://spdx.org/licenses/OLDAP-2.5.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SNIA.json", - "referenceNumber": 368, - "name": "SNIA Public License 1.1", - "licenseId": "SNIA", + "detailsUrl": "https://spdx.org/licenses/OLDAP-2.5.json", + "referenceNumber": 466, + "name": "Open LDAP Public License v2.5", + "licenseId": "OLDAP-2.5", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/SNIA_Public_License" + "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d6852b9d90022e8593c98205413380536b1b5a7cf" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-SA-3.0-IGO.html", + "reference": "https://spdx.org/licenses/OLDAP-2.6.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-3.0-IGO.json", - "referenceNumber": 369, - "name": "Creative Commons Attribution Non Commercial Share Alike 3.0 IGO", - "licenseId": "CC-BY-NC-SA-3.0-IGO", + "detailsUrl": "https://spdx.org/licenses/OLDAP-2.6.json", + "referenceNumber": 384, + "name": "Open LDAP Public License v2.6", + "licenseId": "OLDAP-2.6", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc-sa/3.0/igo/legalcode" + "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d1cae062821881f41b73012ba816434897abf4205" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/AGPL-3.0.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/AGPL-3.0.json", - "referenceNumber": 370, - "name": "GNU Affero General Public License v3.0", - "licenseId": "AGPL-3.0", + "reference": "https://spdx.org/licenses/OLDAP-2.7.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/OLDAP-2.7.json", + "referenceNumber": 414, + "name": "Open LDAP Public License v2.7", + "licenseId": "OLDAP-2.7", "seeAlso": [ - "https://www.gnu.org/licenses/agpl.txt", - "https://opensource.org/licenses/AGPL-3.0" + "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d47c2415c1df81556eeb39be6cad458ef87c534a2" ], - "isOsiApproved": true, + "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/SSPL-1.0.html", + "reference": "https://spdx.org/licenses/OLDAP-2.8.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SSPL-1.0.json", - "referenceNumber": 371, - "name": "Server Side Public License, v 1", - "licenseId": "SSPL-1.0", + "detailsUrl": "https://spdx.org/licenses/OLDAP-2.8.json", + "referenceNumber": 354, + "name": "Open LDAP Public License v2.8", + "licenseId": "OLDAP-2.8", "seeAlso": [ - "https://www.mongodb.com/licensing/server-side-public-license" + "http://www.openldap.org/software/release/license.html" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/RPSL-1.0.html", + "reference": "https://spdx.org/licenses/OML.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/RPSL-1.0.json", - "referenceNumber": 372, - "name": "RealNetworks Public Source License v1.0", - "licenseId": "RPSL-1.0", + "detailsUrl": "https://spdx.org/licenses/OML.json", + "referenceNumber": 464, + "name": "Open Market License", + "licenseId": "OML", "seeAlso": [ - "https://helixcommunity.org/content/rpsl", - "https://opensource.org/licenses/RPSL-1.0" + "https://fedoraproject.org/wiki/Licensing/Open_Market_License" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/MIT-open-group.html", + "reference": "https://spdx.org/licenses/OpenSSL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MIT-open-group.json", - "referenceNumber": 373, - "name": "MIT Open Group variant", - "licenseId": "MIT-open-group", + "detailsUrl": "https://spdx.org/licenses/OpenSSL.json", + "referenceNumber": 402, + "name": "OpenSSL License", + "licenseId": "OpenSSL", "seeAlso": [ - "https://gitlab.freedesktop.org/xorg/app/iceauth/-/blob/master/COPYING", - "https://gitlab.freedesktop.org/xorg/app/xvinfo/-/blob/master/COPYING", - "https://gitlab.freedesktop.org/xorg/app/xsetroot/-/blob/master/COPYING", - "https://gitlab.freedesktop.org/xorg/app/xauth/-/blob/master/COPYING" + "http://www.openssl.org/source/license.html" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/BSD-3-Clause-LBNL.html", + "reference": "https://spdx.org/licenses/OPL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-LBNL.json", - "referenceNumber": 374, - "name": "Lawrence Berkeley National Labs BSD variant license", - "licenseId": "BSD-3-Clause-LBNL", + "detailsUrl": "https://spdx.org/licenses/OPL-1.0.json", + "referenceNumber": 142, + "name": "Open Public License v1.0", + "licenseId": "OPL-1.0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/LBNLBSD" + "http://old.koalateam.com/jackaroo/OPL_1_0.TXT", + "https://fedoraproject.org/wiki/Licensing/Open_Public_License" ], - "isOsiApproved": true + "isOsiApproved": false, + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/GPL-1.0.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GPL-1.0.json", + "reference": "https://spdx.org/licenses/OPUBL-1.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/OPUBL-1.0.json", "referenceNumber": 375, - "name": "GNU General Public License v1.0 only", - "licenseId": "GPL-1.0", + "name": "Open Publication License v1.0", + "licenseId": "OPUBL-1.0", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/gpl-1.0-standalone.html" + "http://opencontent.org/openpub/", + "https://www.debian.org/opl", + "https://www.ctan.org/license/opl" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CECILL-B.html", + "reference": "https://spdx.org/licenses/OSET-PL-2.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CECILL-B.json", - "referenceNumber": 376, - "name": "CeCILL-B Free Software License Agreement", - "licenseId": "CECILL-B", + "detailsUrl": "https://spdx.org/licenses/OSET-PL-2.1.json", + "referenceNumber": 50, + "name": "OSET Public License version 2.1", + "licenseId": "OSET-PL-2.1", "seeAlso": [ - "http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html" + "http://www.osetfoundation.org/public-license", + "https://opensource.org/licenses/OPL-2.1" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/Frameworx-1.0.html", + "reference": "https://spdx.org/licenses/OSL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Frameworx-1.0.json", - "referenceNumber": 377, - "name": "Frameworx Open License 1.0", - "licenseId": "Frameworx-1.0", + "detailsUrl": "https://spdx.org/licenses/OSL-1.0.json", + "referenceNumber": 403, + "name": "Open Software License 1.0", + "licenseId": "OSL-1.0", "seeAlso": [ - "https://opensource.org/licenses/Frameworx-1.0" + "https://opensource.org/licenses/OSL-1.0" ], - "isOsiApproved": true + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/RHeCos-1.1.html", + "reference": "https://spdx.org/licenses/OSL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/RHeCos-1.1.json", - "referenceNumber": 378, - "name": "Red Hat eCos Public License v1.1", - "licenseId": "RHeCos-1.1", + "detailsUrl": "https://spdx.org/licenses/OSL-1.1.json", + "referenceNumber": 400, + "name": "Open Software License 1.1", + "licenseId": "OSL-1.1", "seeAlso": [ - "http://ecos.sourceware.org/old-license.html" + "https://fedoraproject.org/wiki/Licensing/OSL1.1" ], "isOsiApproved": false, - "isFsfLibre": false + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/LAL-1.2.html", + "reference": "https://spdx.org/licenses/OSL-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LAL-1.2.json", - "referenceNumber": 379, - "name": "Licence Art Libre 1.2", - "licenseId": "LAL-1.2", + "detailsUrl": "https://spdx.org/licenses/OSL-2.0.json", + "referenceNumber": 364, + "name": "Open Software License 2.0", + "licenseId": "OSL-2.0", "seeAlso": [ - "http://artlibre.org/licence/lal/licence-art-libre-12/" + "http://web.archive.org/web/20041020171434/http://www.rosenlaw.com/osl2.0.html" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/OML.html", + "reference": "https://spdx.org/licenses/OSL-2.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OML.json", - "referenceNumber": 380, - "name": "Open Market License", - "licenseId": "OML", + "detailsUrl": "https://spdx.org/licenses/OSL-2.1.json", + "referenceNumber": 159, + "name": "Open Software License 2.1", + "licenseId": "OSL-2.1", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Open_Market_License" + "http://web.archive.org/web/20050212003940/http://www.rosenlaw.com/osl21.htm", + "https://opensource.org/licenses/OSL-2.1" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/UCL-1.0.html", + "reference": "https://spdx.org/licenses/OSL-3.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/UCL-1.0.json", - "referenceNumber": 381, - "name": "Upstream Compatibility License v1.0", - "licenseId": "UCL-1.0", - "seeAlso": [ - "https://opensource.org/licenses/UCL-1.0" - ], - "isOsiApproved": true - }, - { - "reference": "https://spdx.org/licenses/GPL-2.0-with-autoconf-exception.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GPL-2.0-with-autoconf-exception.json", - "referenceNumber": 382, - "name": "GNU General Public License v2.0 w/Autoconf exception", - "licenseId": "GPL-2.0-with-autoconf-exception", + "detailsUrl": "https://spdx.org/licenses/OSL-3.0.json", + "referenceNumber": 342, + "name": "Open Software License 3.0", + "licenseId": "OSL-3.0", "seeAlso": [ - "http://ac-archive.sourceforge.net/doc/copyright.html" + "https://web.archive.org/web/20120101081418/http://rosenlaw.com:80/OSL3.0.htm", + "https://opensource.org/licenses/OSL-3.0" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/APL-1.0.html", + "reference": "https://spdx.org/licenses/Parity-6.0.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/APL-1.0.json", - "referenceNumber": 383, - "name": "Adaptive Public License 1.0", - "licenseId": "APL-1.0", + "detailsUrl": "https://spdx.org/licenses/Parity-6.0.0.json", + "referenceNumber": 102, + "name": "The Parity Public License 6.0.0", + "licenseId": "Parity-6.0.0", "seeAlso": [ - "https://opensource.org/licenses/APL-1.0" + "https://paritylicense.com/versions/6.0.0.html" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OLDAP-2.2.html", + "reference": "https://spdx.org/licenses/Parity-7.0.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OLDAP-2.2.json", - "referenceNumber": 384, - "name": "Open LDAP Public License v2.2", - "licenseId": "OLDAP-2.2", + "detailsUrl": "https://spdx.org/licenses/Parity-7.0.0.json", + "referenceNumber": 51, + "name": "The Parity Public License 7.0.0", + "licenseId": "Parity-7.0.0", "seeAlso": [ - "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d470b0c18ec67621c85881b2733057fecf4a1acc3" + "https://paritylicense.com/versions/7.0.0.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CERN-OHL-W-2.0.html", + "reference": "https://spdx.org/licenses/PDDL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CERN-OHL-W-2.0.json", - "referenceNumber": 385, - "name": "CERN Open Hardware Licence Version 2 - Weakly Reciprocal", - "licenseId": "CERN-OHL-W-2.0", + "detailsUrl": "https://spdx.org/licenses/PDDL-1.0.json", + "referenceNumber": 87, + "name": "Open Data Commons Public Domain Dedication \u0026 License 1.0", + "licenseId": "PDDL-1.0", "seeAlso": [ - "https://www.ohwr.org/project/cernohl/wikis/Documents/CERN-OHL-version-2" + "http://opendatacommons.org/licenses/pddl/1.0/", + "https://opendatacommons.org/licenses/pddl/" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/DRL-1.0.html", + "reference": "https://spdx.org/licenses/PHP-3.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/DRL-1.0.json", - "referenceNumber": 386, - "name": "Detection Rule License 1.0", - "licenseId": "DRL-1.0", + "detailsUrl": "https://spdx.org/licenses/PHP-3.0.json", + "referenceNumber": 191, + "name": "PHP License v3.0", + "licenseId": "PHP-3.0", "seeAlso": [ - "https://github.com/Neo23x0/sigma/blob/master/LICENSE.Detection.Rules.md" + "http://www.php.net/license/3_0.txt", + "https://opensource.org/licenses/PHP-3.0" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/GPL-3.0+.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GPL-3.0+.json", - "referenceNumber": 387, - "name": "GNU General Public License v3.0 or later", - "licenseId": "GPL-3.0+", + "reference": "https://spdx.org/licenses/PHP-3.01.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/PHP-3.01.json", + "referenceNumber": 469, + "name": "PHP License v3.01", + "licenseId": "PHP-3.01", "seeAlso": [ - "https://www.gnu.org/licenses/gpl-3.0-standalone.html", - "https://opensource.org/licenses/GPL-3.0" + "http://www.php.net/license/3_01.txt" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CC-BY-ND-1.0.html", + "reference": "https://spdx.org/licenses/Plexus.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-ND-1.0.json", - "referenceNumber": 388, - "name": "Creative Commons Attribution No Derivatives 1.0 Generic", - "licenseId": "CC-BY-ND-1.0", + "detailsUrl": "https://spdx.org/licenses/Plexus.json", + "referenceNumber": 472, + "name": "Plexus Classworlds License", + "licenseId": "Plexus", "seeAlso": [ - "https://creativecommons.org/licenses/by-nd/1.0/legalcode" + "https://fedoraproject.org/wiki/Licensing/Plexus_Classworlds_License" ], - "isOsiApproved": false, - "isFsfLibre": false + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GFDL-1.3-no-invariants-or-later.html", + "reference": "https://spdx.org/licenses/PolyForm-Noncommercial-1.0.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.3-no-invariants-or-later.json", - "referenceNumber": 389, - "name": "GNU Free Documentation License v1.3 or later - no invariants", - "licenseId": "GFDL-1.3-no-invariants-or-later", + "detailsUrl": "https://spdx.org/licenses/PolyForm-Noncommercial-1.0.0.json", + "referenceNumber": 188, + "name": "PolyForm Noncommercial License 1.0.0", + "licenseId": "PolyForm-Noncommercial-1.0.0", "seeAlso": [ - "https://www.gnu.org/licenses/fdl-1.3.txt" + "https://polyformproject.org/licenses/noncommercial/1.0.0" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CECILL-1.1.html", + "reference": "https://spdx.org/licenses/PolyForm-Small-Business-1.0.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CECILL-1.1.json", - "referenceNumber": 390, - "name": "CeCILL Free Software License Agreement v1.1", - "licenseId": "CECILL-1.1", + "detailsUrl": "https://spdx.org/licenses/PolyForm-Small-Business-1.0.0.json", + "referenceNumber": 302, + "name": "PolyForm Small Business License 1.0.0", + "licenseId": "PolyForm-Small-Business-1.0.0", "seeAlso": [ - "http://www.cecill.info/licences/Licence_CeCILL_V1.1-US.html" + "https://polyformproject.org/licenses/small-business/1.0.0" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Noweb.html", + "reference": "https://spdx.org/licenses/PostgreSQL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Noweb.json", - "referenceNumber": 391, - "name": "Noweb License", - "licenseId": "Noweb", + "detailsUrl": "https://spdx.org/licenses/PostgreSQL.json", + "referenceNumber": 237, + "name": "PostgreSQL License", + "licenseId": "PostgreSQL", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Noweb" + "http://www.postgresql.org/about/licence", + "https://opensource.org/licenses/PostgreSQL" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/MakeIndex.html", + "reference": "https://spdx.org/licenses/PSF-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MakeIndex.json", - "referenceNumber": 392, - "name": "MakeIndex License", - "licenseId": "MakeIndex", + "detailsUrl": "https://spdx.org/licenses/PSF-2.0.json", + "referenceNumber": 452, + "name": "Python Software Foundation License 2.0", + "licenseId": "PSF-2.0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/MakeIndex" + "https://opensource.org/licenses/Python-2.0" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/MS-RL.html", + "reference": "https://spdx.org/licenses/psfrag.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MS-RL.json", - "referenceNumber": 393, - "name": "Microsoft Reciprocal License", - "licenseId": "MS-RL", + "detailsUrl": "https://spdx.org/licenses/psfrag.json", + "referenceNumber": 268, + "name": "psfrag License", + "licenseId": "psfrag", "seeAlso": [ - "http://www.microsoft.com/opensource/licenses.mspx", - "https://opensource.org/licenses/MS-RL" + "https://fedoraproject.org/wiki/Licensing/psfrag" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/TORQUE-1.1.html", + "reference": "https://spdx.org/licenses/psutils.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/TORQUE-1.1.json", - "referenceNumber": 394, - "name": "TORQUE v2.5+ Software License v1.1", - "licenseId": "TORQUE-1.1", + "detailsUrl": "https://spdx.org/licenses/psutils.json", + "referenceNumber": 106, + "name": "psutils License", + "licenseId": "psutils", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/TORQUEv1.1" + "https://fedoraproject.org/wiki/Licensing/psutils" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/AFL-3.0.html", + "reference": "https://spdx.org/licenses/Python-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/AFL-3.0.json", - "referenceNumber": 395, - "name": "Academic Free License v3.0", - "licenseId": "AFL-3.0", + "detailsUrl": "https://spdx.org/licenses/Python-2.0.json", + "referenceNumber": 368, + "name": "Python License 2.0", + "licenseId": "Python-2.0", "seeAlso": [ - "http://www.rosenlaw.com/AFL3.0.htm", - "https://opensource.org/licenses/afl-3.0" + "https://opensource.org/licenses/Python-2.0" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/GFDL-1.1-invariants-only.html", + "reference": "https://spdx.org/licenses/Python-2.0.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.1-invariants-only.json", - "referenceNumber": 396, - "name": "GNU Free Documentation License v1.1 only - invariants", - "licenseId": "GFDL-1.1-invariants-only", + "detailsUrl": "https://spdx.org/licenses/Python-2.0.1.json", + "referenceNumber": 200, + "name": "Python License 2.0.1", + "licenseId": "Python-2.0.1", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" + "https://www.python.org/download/releases/2.0.1/license/", + "https://docs.python.org/3/license.html", + "https://github.com/python/cpython/blob/main/LICENSE" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GFDL-1.1.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.1.json", - "referenceNumber": 397, - "name": "GNU Free Documentation License v1.1", - "licenseId": "GFDL-1.1", + "reference": "https://spdx.org/licenses/Qhull.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/Qhull.json", + "referenceNumber": 270, + "name": "Qhull License", + "licenseId": "Qhull", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/fdl-1.1.txt" + "https://fedoraproject.org/wiki/Licensing/Qhull" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/FTL.html", + "reference": "https://spdx.org/licenses/QPL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/FTL.json", - "referenceNumber": 398, - "name": "Freetype Project License", - "licenseId": "FTL", + "detailsUrl": "https://spdx.org/licenses/QPL-1.0.json", + "referenceNumber": 57, + "name": "Q Public License 1.0", + "licenseId": "QPL-1.0", "seeAlso": [ - "http://freetype.fis.uniroma2.it/FTL.TXT", - "http://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/docs/FTL.TXT", - "http://gitlab.freedesktop.org/freetype/freetype/-/raw/master/docs/FTL.TXT" + "http://doc.qt.nokia.com/3.3/license.html", + "https://opensource.org/licenses/QPL-1.0", + "https://doc.qt.io/archives/3.3/license.html" ], - "isOsiApproved": false, + "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/AFL-2.0.html", + "reference": "https://spdx.org/licenses/Rdisc.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/AFL-2.0.json", - "referenceNumber": 399, - "name": "Academic Free License v2.0", - "licenseId": "AFL-2.0", + "detailsUrl": "https://spdx.org/licenses/Rdisc.json", + "referenceNumber": 14, + "name": "Rdisc License", + "licenseId": "Rdisc", "seeAlso": [ - "http://wayback.archive.org/web/20060924134533/http://www.opensource.org/licenses/afl-2.0.txt" + "https://fedoraproject.org/wiki/Licensing/Rdisc_License" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GPL-3.0-with-autoconf-exception.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/GPL-3.0-with-autoconf-exception.json", - "referenceNumber": 400, - "name": "GNU General Public License v3.0 w/Autoconf exception", - "licenseId": "GPL-3.0-with-autoconf-exception", + "reference": "https://spdx.org/licenses/RHeCos-1.1.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/RHeCos-1.1.json", + "referenceNumber": 224, + "name": "Red Hat eCos Public License v1.1", + "licenseId": "RHeCos-1.1", "seeAlso": [ - "https://www.gnu.org/licenses/autoconf-exception-3.0.html" + "http://ecos.sourceware.org/old-license.html" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/LPPL-1.3a.html", + "reference": "https://spdx.org/licenses/RPL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LPPL-1.3a.json", - "referenceNumber": 401, - "name": "LaTeX Project Public License v1.3a", - "licenseId": "LPPL-1.3a", + "detailsUrl": "https://spdx.org/licenses/RPL-1.1.json", + "referenceNumber": 54, + "name": "Reciprocal Public License 1.1", + "licenseId": "RPL-1.1", "seeAlso": [ - "http://www.latex-project.org/lppl/lppl-1-3a.txt" + "https://opensource.org/licenses/RPL-1.1" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/OLDAP-1.2.html", + "reference": "https://spdx.org/licenses/RPL-1.5.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OLDAP-1.2.json", - "referenceNumber": 402, - "name": "Open LDAP Public License v1.2", - "licenseId": "OLDAP-1.2", + "detailsUrl": "https://spdx.org/licenses/RPL-1.5.json", + "referenceNumber": 36, + "name": "Reciprocal Public License 1.5", + "licenseId": "RPL-1.5", "seeAlso": [ - "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d42b0383c50c299977b5893ee695cf4e486fb0dc7" + "https://opensource.org/licenses/RPL-1.5" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/ODbL-1.0.html", + "reference": "https://spdx.org/licenses/RPSL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/ODbL-1.0.json", - "referenceNumber": 403, - "name": "Open Data Commons Open Database License v1.0", - "licenseId": "ODbL-1.0", + "detailsUrl": "https://spdx.org/licenses/RPSL-1.0.json", + "referenceNumber": 324, + "name": "RealNetworks Public Source License v1.0", + "licenseId": "RPSL-1.0", "seeAlso": [ - "http://www.opendatacommons.org/licenses/odbl/1.0/", - "https://opendatacommons.org/licenses/odbl/1-0/" + "https://helixcommunity.org/content/rpsl", + "https://opensource.org/licenses/RPSL-1.0" ], - "isOsiApproved": false, + "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/LiLiQ-Rplus-1.1.html", + "reference": "https://spdx.org/licenses/RSA-MD.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/RSA-MD.json", + "referenceNumber": 47, + "name": "RSA Message-Digest License", + "licenseId": "RSA-MD", + "seeAlso": [ + "http://www.faqs.org/rfcs/rfc1321.html" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/RSCPL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LiLiQ-Rplus-1.1.json", - "referenceNumber": 404, - "name": "Licence Libre du Québec – Réciprocité forte version 1.1", - "licenseId": "LiLiQ-Rplus-1.1", + "detailsUrl": "https://spdx.org/licenses/RSCPL.json", + "referenceNumber": 218, + "name": "Ricoh Source Code Public License", + "licenseId": "RSCPL", "seeAlso": [ - "https://www.forge.gouv.qc.ca/participez/licence-logicielle/licence-libre-du-quebec-liliq-en-francais/licence-libre-du-quebec-reciprocite-forte-liliq-r-v1-1/", - "http://opensource.org/licenses/LiLiQ-Rplus-1.1" + "http://wayback.archive.org/web/20060715140826/http://www.risource.org/RPL/RPL-1.0A.shtml", + "https://opensource.org/licenses/RSCPL" ], "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/Qhull.html", + "reference": "https://spdx.org/licenses/Ruby.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Qhull.json", - "referenceNumber": 405, - "name": "Qhull License", - "licenseId": "Qhull", + "detailsUrl": "https://spdx.org/licenses/Ruby.json", + "referenceNumber": 395, + "name": "Ruby License", + "licenseId": "Ruby", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Qhull" + "http://www.ruby-lang.org/en/LICENSE.txt" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/OCCT-PL.html", + "reference": "https://spdx.org/licenses/SAX-PD.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OCCT-PL.json", - "referenceNumber": 406, - "name": "Open CASCADE Technology Public License", - "licenseId": "OCCT-PL", + "detailsUrl": "https://spdx.org/licenses/SAX-PD.json", + "referenceNumber": 67, + "name": "Sax Public Domain Notice", + "licenseId": "SAX-PD", "seeAlso": [ - "http://www.opencascade.com/content/occt-public-license" + "http://www.saxproject.org/copying.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/NTP.html", + "reference": "https://spdx.org/licenses/Saxpath.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NTP.json", - "referenceNumber": 407, - "name": "NTP License", - "licenseId": "NTP", + "detailsUrl": "https://spdx.org/licenses/Saxpath.json", + "referenceNumber": 127, + "name": "Saxpath License", + "licenseId": "Saxpath", "seeAlso": [ - "https://opensource.org/licenses/NTP" + "https://fedoraproject.org/wiki/Licensing/Saxpath_License" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/EUPL-1.1.html", + "reference": "https://spdx.org/licenses/SCEA.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/EUPL-1.1.json", - "referenceNumber": 408, - "name": "European Union Public License 1.1", - "licenseId": "EUPL-1.1", + "detailsUrl": "https://spdx.org/licenses/SCEA.json", + "referenceNumber": 95, + "name": "SCEA Shared Source License", + "licenseId": "SCEA", "seeAlso": [ - "https://joinup.ec.europa.eu/software/page/eupl/licence-eupl", - "https://joinup.ec.europa.eu/sites/default/files/custom-page/attachment/eupl1.1.-licence-en_0.pdf", - "https://opensource.org/licenses/EUPL-1.1" + "http://research.scea.com/scea_shared_source_license.html" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CPOL-1.02.html", + "reference": "https://spdx.org/licenses/SchemeReport.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CPOL-1.02.json", - "referenceNumber": 409, - "name": "Code Project Open License 1.02", - "licenseId": "CPOL-1.02", - "seeAlso": [ - "http://www.codeproject.com/info/cpol10.aspx" - ], - "isOsiApproved": false, - "isFsfLibre": false + "detailsUrl": "https://spdx.org/licenses/SchemeReport.json", + "referenceNumber": 178, + "name": "Scheme Language Report License", + "licenseId": "SchemeReport", + "seeAlso": [], + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OCLC-2.0.html", + "reference": "https://spdx.org/licenses/Sendmail.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OCLC-2.0.json", - "referenceNumber": 410, - "name": "OCLC Research Public License 2.0", - "licenseId": "OCLC-2.0", + "detailsUrl": "https://spdx.org/licenses/Sendmail.json", + "referenceNumber": 422, + "name": "Sendmail License", + "licenseId": "Sendmail", "seeAlso": [ - "http://www.oclc.org/research/activities/software/license/v2final.htm", - "https://opensource.org/licenses/OCLC-2.0" + "http://www.sendmail.com/pdfs/open_source/sendmail_license.pdf", + "https://web.archive.org/web/20160322142305/https://www.sendmail.com/pdfs/open_source/sendmail_license.pdf" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OLDAP-2.1.html", + "reference": "https://spdx.org/licenses/Sendmail-8.23.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OLDAP-2.1.json", - "referenceNumber": 411, - "name": "Open LDAP Public License v2.1", - "licenseId": "OLDAP-2.1", + "detailsUrl": "https://spdx.org/licenses/Sendmail-8.23.json", + "referenceNumber": 500, + "name": "Sendmail License 8.23", + "licenseId": "Sendmail-8.23", "seeAlso": [ - "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003db0d176738e96a0d3b9f85cb51e140a86f21be715" + "https://www.proofpoint.com/sites/default/files/sendmail-license.pdf", + "https://web.archive.org/web/20181003101040/https://www.proofpoint.com/sites/default/files/sendmail-license.pdf" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Intel-ACPI.html", + "reference": "https://spdx.org/licenses/SGI-B-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Intel-ACPI.json", - "referenceNumber": 412, - "name": "Intel ACPI Software License Agreement", - "licenseId": "Intel-ACPI", + "detailsUrl": "https://spdx.org/licenses/SGI-B-1.0.json", + "referenceNumber": 93, + "name": "SGI Free Software License B v1.0", + "licenseId": "SGI-B-1.0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Intel_ACPI_Software_License_Agreement" + "http://oss.sgi.com/projects/FreeB/SGIFreeSWLicB.1.0.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OFL-1.0-RFN.html", + "reference": "https://spdx.org/licenses/SGI-B-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OFL-1.0-RFN.json", - "referenceNumber": 413, - "name": "SIL Open Font License 1.0 with Reserved Font Name", - "licenseId": "OFL-1.0-RFN", + "detailsUrl": "https://spdx.org/licenses/SGI-B-1.1.json", + "referenceNumber": 68, + "name": "SGI Free Software License B v1.1", + "licenseId": "SGI-B-1.1", "seeAlso": [ - "http://scripts.sil.org/cms/scripts/page.php?item_id\u003dOFL10_web" + "http://oss.sgi.com/projects/FreeB/" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC0-1.0.html", + "reference": "https://spdx.org/licenses/SGI-B-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC0-1.0.json", - "referenceNumber": 414, - "name": "Creative Commons Zero v1.0 Universal", - "licenseId": "CC0-1.0", + "detailsUrl": "https://spdx.org/licenses/SGI-B-2.0.json", + "referenceNumber": 352, + "name": "SGI Free Software License B v2.0", + "licenseId": "SGI-B-2.0", "seeAlso": [ - "https://creativecommons.org/publicdomain/zero/1.0/legalcode" + "http://oss.sgi.com/projects/FreeB/SGIFreeSWLicB.2.0.pdf" ], "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/LGPL-2.0-or-later.html", + "reference": "https://spdx.org/licenses/SHL-0.5.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LGPL-2.0-or-later.json", - "referenceNumber": 415, - "name": "GNU Library General Public License v2 or later", - "licenseId": "LGPL-2.0-or-later", + "detailsUrl": "https://spdx.org/licenses/SHL-0.5.json", + "referenceNumber": 88, + "name": "Solderpad Hardware License v0.5", + "licenseId": "SHL-0.5", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/lgpl-2.0-standalone.html" + "https://solderpad.org/licenses/SHL-0.5/" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/SGI-B-1.1.html", + "reference": "https://spdx.org/licenses/SHL-0.51.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/SGI-B-1.1.json", - "referenceNumber": 416, - "name": "SGI Free Software License B v1.1", - "licenseId": "SGI-B-1.1", + "detailsUrl": "https://spdx.org/licenses/SHL-0.51.json", + "referenceNumber": 345, + "name": "Solderpad Hardware License, Version 0.51", + "licenseId": "SHL-0.51", "seeAlso": [ - "http://oss.sgi.com/projects/FreeB/" + "https://solderpad.org/licenses/SHL-0.51/" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-ND-3.0.html", + "reference": "https://spdx.org/licenses/SimPL-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-ND-3.0.json", - "referenceNumber": 417, - "name": "Creative Commons Attribution Non Commercial No Derivatives 3.0 Unported", - "licenseId": "CC-BY-NC-ND-3.0", + "detailsUrl": "https://spdx.org/licenses/SimPL-2.0.json", + "referenceNumber": 45, + "name": "Simple Public License 2.0", + "licenseId": "SimPL-2.0", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc-nd/3.0/legalcode" + "https://opensource.org/licenses/SimPL-2.0" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/etalab-2.0.html", + "reference": "https://spdx.org/licenses/SISSL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/etalab-2.0.json", - "referenceNumber": 418, - "name": "Etalab Open License 2.0", - "licenseId": "etalab-2.0", + "detailsUrl": "https://spdx.org/licenses/SISSL.json", + "referenceNumber": 86, + "name": "Sun Industry Standards Source License v1.1", + "licenseId": "SISSL", "seeAlso": [ - "https://github.com/DISIC/politique-de-contribution-open-source/blob/master/LICENSE.pdf", - "https://raw.githubusercontent.com/DISIC/politique-de-contribution-open-source/master/LICENSE" + "http://www.openoffice.org/licenses/sissl_license.html", + "https://opensource.org/licenses/SISSL" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-SA-2.0-FR.html", + "reference": "https://spdx.org/licenses/SISSL-1.2.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-SA-2.0-FR.json", - "referenceNumber": 419, - "name": "Creative Commons Attribution-NonCommercial-ShareAlike 2.0 France", - "licenseId": "CC-BY-NC-SA-2.0-FR", + "detailsUrl": "https://spdx.org/licenses/SISSL-1.2.json", + "referenceNumber": 154, + "name": "Sun Industry Standards Source License v1.2", + "licenseId": "SISSL-1.2", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc-sa/2.0/fr/legalcode" + "http://gridscheduler.sourceforge.net/Gridengine_SISSL_license.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GPL-3.0-only.html", + "reference": "https://spdx.org/licenses/Sleepycat.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GPL-3.0-only.json", - "referenceNumber": 420, - "name": "GNU General Public License v3.0 only", - "licenseId": "GPL-3.0-only", + "detailsUrl": "https://spdx.org/licenses/Sleepycat.json", + "referenceNumber": 261, + "name": "Sleepycat License", + "licenseId": "Sleepycat", "seeAlso": [ - "https://www.gnu.org/licenses/gpl-3.0-standalone.html", - "https://opensource.org/licenses/GPL-3.0" + "https://opensource.org/licenses/Sleepycat" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Zend-2.0.html", + "reference": "https://spdx.org/licenses/SMLNJ.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Zend-2.0.json", - "referenceNumber": 421, - "name": "Zend License v2.0", - "licenseId": "Zend-2.0", + "detailsUrl": "https://spdx.org/licenses/SMLNJ.json", + "referenceNumber": 184, + "name": "Standard ML of New Jersey License", + "licenseId": "SMLNJ", "seeAlso": [ - "https://web.archive.org/web/20130517195954/http://www.zend.com/license/2_00.txt" + "https://www.smlnj.org/license.html" ], "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/MirOS.html", + "reference": "https://spdx.org/licenses/SMPPL.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/SMPPL.json", + "referenceNumber": 228, + "name": "Secure Messaging Protocol Public License", + "licenseId": "SMPPL", + "seeAlso": [ + "https://github.com/dcblake/SMP/blob/master/Documentation/License.txt" + ], + "isOsiApproved": false + }, + { + "reference": "https://spdx.org/licenses/SNIA.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MirOS.json", - "referenceNumber": 422, - "name": "The MirOS Licence", - "licenseId": "MirOS", + "detailsUrl": "https://spdx.org/licenses/SNIA.json", + "referenceNumber": 104, + "name": "SNIA Public License 1.1", + "licenseId": "SNIA", "seeAlso": [ - "https://opensource.org/licenses/MirOS" + "https://fedoraproject.org/wiki/Licensing/SNIA_Public_License" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BSD-3-Clause-No-Nuclear-License-2014.html", + "reference": "https://spdx.org/licenses/Spencer-86.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-No-Nuclear-License-2014.json", - "referenceNumber": 423, - "name": "BSD 3-Clause No Nuclear License 2014", - "licenseId": "BSD-3-Clause-No-Nuclear-License-2014", + "detailsUrl": "https://spdx.org/licenses/Spencer-86.json", + "referenceNumber": 351, + "name": "Spencer License 86", + "licenseId": "Spencer-86", "seeAlso": [ - "https://java.net/projects/javaeetutorial/pages/BerkeleyLicense" + "https://fedoraproject.org/wiki/Licensing/Henry_Spencer_Reg-Ex_Library_License" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BitTorrent-1.0.html", + "reference": "https://spdx.org/licenses/Spencer-94.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BitTorrent-1.0.json", - "referenceNumber": 424, - "name": "BitTorrent Open Source License v1.0", - "licenseId": "BitTorrent-1.0", + "detailsUrl": "https://spdx.org/licenses/Spencer-94.json", + "referenceNumber": 144, + "name": "Spencer License 94", + "licenseId": "Spencer-94", "seeAlso": [ - "http://sources.gentoo.org/cgi-bin/viewvc.cgi/gentoo-x86/licenses/BitTorrent?r1\u003d1.1\u0026r2\u003d1.1.1.1\u0026diff_format\u003ds" + "https://fedoraproject.org/wiki/Licensing/Henry_Spencer_Reg-Ex_Library_License" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-ND-3.0.html", + "reference": "https://spdx.org/licenses/Spencer-99.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-ND-3.0.json", - "referenceNumber": 425, - "name": "Creative Commons Attribution No Derivatives 3.0 Unported", - "licenseId": "CC-BY-ND-3.0", + "detailsUrl": "https://spdx.org/licenses/Spencer-99.json", + "referenceNumber": 233, + "name": "Spencer License 99", + "licenseId": "Spencer-99", "seeAlso": [ - "https://creativecommons.org/licenses/by-nd/3.0/legalcode" + "http://www.opensource.apple.com/source/tcl/tcl-5/tcl/generic/regfronts.c" ], - "isOsiApproved": false, - "isFsfLibre": false + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/MIT.html", + "reference": "https://spdx.org/licenses/SPL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MIT.json", - "referenceNumber": 426, - "name": "MIT License", - "licenseId": "MIT", + "detailsUrl": "https://spdx.org/licenses/SPL-1.0.json", + "referenceNumber": 124, + "name": "Sun Public License v1.0", + "licenseId": "SPL-1.0", "seeAlso": [ - "https://opensource.org/licenses/MIT" + "https://opensource.org/licenses/SPL-1.0" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/NIST-PD-fallback.html", + "reference": "https://spdx.org/licenses/SSH-OpenSSH.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NIST-PD-fallback.json", - "referenceNumber": 427, - "name": "NIST Public Domain Notice with license fallback", - "licenseId": "NIST-PD-fallback", + "detailsUrl": "https://spdx.org/licenses/SSH-OpenSSH.json", + "referenceNumber": 494, + "name": "SSH OpenSSH license", + "licenseId": "SSH-OpenSSH", "seeAlso": [ - "https://github.com/usnistgov/jsip/blob/59700e6926cbe96c5cdae897d9a7d2656b42abe3/LICENSE", - "https://github.com/usnistgov/fipy/blob/86aaa5c2ba2c6f1be19593c5986071cf6568cc34/LICENSE.rst" + "https://github.com/openssh/openssh-portable/blob/1b11ea7c58cd5c59838b5fa574cd456d6047b2d4/LICENCE#L10" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-3.0-DE.html", + "reference": "https://spdx.org/licenses/SSH-short.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-3.0-DE.json", - "referenceNumber": 428, - "name": "Creative Commons Attribution Non Commercial 3.0 Germany", - "licenseId": "CC-BY-NC-3.0-DE", + "detailsUrl": "https://spdx.org/licenses/SSH-short.json", + "referenceNumber": 165, + "name": "SSH short notice", + "licenseId": "SSH-short", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc/3.0/de/legalcode" + "https://github.com/openssh/openssh-portable/blob/1b11ea7c58cd5c59838b5fa574cd456d6047b2d4/pathnames.h", + "http://web.mit.edu/kolya/.f/root/athena.mit.edu/sipb.mit.edu/project/openssh/OldFiles/src/openssh-2.9.9p2/ssh-add.1", + "https://joinup.ec.europa.eu/svn/lesoll/trunk/italc/lib/src/dsa_key.cpp" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/FSFULLR.html", + "reference": "https://spdx.org/licenses/SSPL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/FSFULLR.json", - "referenceNumber": 429, - "name": "FSF Unlimited License (with License Retention)", - "licenseId": "FSFULLR", + "detailsUrl": "https://spdx.org/licenses/SSPL-1.0.json", + "referenceNumber": 77, + "name": "Server Side Public License, v 1", + "licenseId": "SSPL-1.0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/FSF_Unlimited_License#License_Retention_Variant" + "https://www.mongodb.com/licensing/server-side-public-license" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-NC-1.0.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-NC-1.0.json", - "referenceNumber": 430, - "name": "Creative Commons Attribution Non Commercial 1.0 Generic", - "licenseId": "CC-BY-NC-1.0", + "reference": "https://spdx.org/licenses/StandardML-NJ.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/StandardML-NJ.json", + "referenceNumber": 501, + "name": "Standard ML of New Jersey License", + "licenseId": "StandardML-NJ", "seeAlso": [ - "https://creativecommons.org/licenses/by-nc/1.0/legalcode" + "https://www.smlnj.org/license.html" ], "isOsiApproved": false, - "isFsfLibre": false + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Python-2.0.html", + "reference": "https://spdx.org/licenses/SugarCRM-1.1.3.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Python-2.0.json", - "referenceNumber": 431, - "name": "Python License 2.0", - "licenseId": "Python-2.0", + "detailsUrl": "https://spdx.org/licenses/SugarCRM-1.1.3.json", + "referenceNumber": 91, + "name": "SugarCRM Public License v1.1.3", + "licenseId": "SugarCRM-1.1.3", "seeAlso": [ - "https://opensource.org/licenses/Python-2.0" + "http://www.sugarcrm.com/crm/SPL" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/ANTLR-PD-fallback.html", + "reference": "https://spdx.org/licenses/SWL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/ANTLR-PD-fallback.json", - "referenceNumber": 432, - "name": "ANTLR Software Rights Notice with license fallback", - "licenseId": "ANTLR-PD-fallback", + "detailsUrl": "https://spdx.org/licenses/SWL.json", + "referenceNumber": 293, + "name": "Scheme Widget Library (SWL) Software License Agreement", + "licenseId": "SWL", "seeAlso": [ - "http://www.antlr2.org/license.html" + "https://fedoraproject.org/wiki/Licensing/SWL" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/MulanPSL-1.0.html", + "reference": "https://spdx.org/licenses/Symlinks.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MulanPSL-1.0.json", - "referenceNumber": 433, - "name": "Mulan Permissive Software License, Version 1", - "licenseId": "MulanPSL-1.0", + "detailsUrl": "https://spdx.org/licenses/Symlinks.json", + "referenceNumber": 457, + "name": "Symlinks License", + "licenseId": "Symlinks", "seeAlso": [ - "https://license.coscl.org.cn/MulanPSL/", - "https://github.com/yuwenlong/longphp/blob/25dfb70cc2a466dc4bb55ba30901cbce08d164b5/LICENSE" + "https://www.mail-archive.com/debian-bugs-rc@lists.debian.org/msg11494.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/wxWindows.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/wxWindows.json", - "referenceNumber": 434, - "name": "wxWindows Library License", - "licenseId": "wxWindows", + "reference": "https://spdx.org/licenses/TAPR-OHL-1.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/TAPR-OHL-1.0.json", + "referenceNumber": 230, + "name": "TAPR Open Hardware License v1.0", + "licenseId": "TAPR-OHL-1.0", "seeAlso": [ - "https://opensource.org/licenses/WXwindows" + "https://www.tapr.org/OHL" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CC-BY-4.0.html", + "reference": "https://spdx.org/licenses/TCL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-4.0.json", - "referenceNumber": 435, - "name": "Creative Commons Attribution 4.0 International", - "licenseId": "CC-BY-4.0", + "detailsUrl": "https://spdx.org/licenses/TCL.json", + "referenceNumber": 94, + "name": "TCL/TK License", + "licenseId": "TCL", "seeAlso": [ - "https://creativecommons.org/licenses/by/4.0/legalcode" + "http://www.tcl.tk/software/tcltk/license.html", + "https://fedoraproject.org/wiki/Licensing/TCL" ], - "isOsiApproved": false, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Rdisc.html", + "reference": "https://spdx.org/licenses/TCP-wrappers.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Rdisc.json", - "referenceNumber": 436, - "name": "Rdisc License", - "licenseId": "Rdisc", + "detailsUrl": "https://spdx.org/licenses/TCP-wrappers.json", + "referenceNumber": 416, + "name": "TCP Wrappers License", + "licenseId": "TCP-wrappers", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Rdisc_License" + "http://rc.quest.com/topics/openssh/license.php#tcpwrappers" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/MIT-enna.html", + "reference": "https://spdx.org/licenses/TMate.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MIT-enna.json", - "referenceNumber": 437, - "name": "enna License", - "licenseId": "MIT-enna", + "detailsUrl": "https://spdx.org/licenses/TMate.json", + "referenceNumber": 295, + "name": "TMate Open Source License", + "licenseId": "TMate", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/MIT#enna" + "http://svnkit.com/license.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/BSD-4-Clause-UC.html", + "reference": "https://spdx.org/licenses/TORQUE-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-4-Clause-UC.json", - "referenceNumber": 438, - "name": "BSD-4-Clause (University of California-Specific)", - "licenseId": "BSD-4-Clause-UC", + "detailsUrl": "https://spdx.org/licenses/TORQUE-1.1.json", + "referenceNumber": 421, + "name": "TORQUE v2.5+ Software License v1.1", + "licenseId": "TORQUE-1.1", "seeAlso": [ - "http://www.freebsd.org/copyright/license.html" + "https://fedoraproject.org/wiki/Licensing/TORQUEv1.1" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/MPL-2.0.html", + "reference": "https://spdx.org/licenses/TOSL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MPL-2.0.json", - "referenceNumber": 439, - "name": "Mozilla Public License 2.0", - "licenseId": "MPL-2.0", + "detailsUrl": "https://spdx.org/licenses/TOSL.json", + "referenceNumber": 304, + "name": "Trusster Open Source License", + "licenseId": "TOSL", "seeAlso": [ - "https://www.mozilla.org/MPL/2.0/", - "https://opensource.org/licenses/MPL-2.0" + "https://fedoraproject.org/wiki/Licensing/TOSL" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/YPL-1.0.html", + "reference": "https://spdx.org/licenses/TPDL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/YPL-1.0.json", - "referenceNumber": 440, - "name": "Yahoo! Public License v1.0", - "licenseId": "YPL-1.0", + "detailsUrl": "https://spdx.org/licenses/TPDL.json", + "referenceNumber": 41, + "name": "Time::ParseDate License", + "licenseId": "TPDL", "seeAlso": [ - "http://www.zimbra.com/license/yahoo_public_license_1.0.html" + "https://metacpan.org/pod/Time::ParseDate#LICENSE" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/ZPL-2.0.html", + "reference": "https://spdx.org/licenses/TTWL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/ZPL-2.0.json", - "referenceNumber": 441, - "name": "Zope Public License 2.0", - "licenseId": "ZPL-2.0", + "detailsUrl": "https://spdx.org/licenses/TTWL.json", + "referenceNumber": 251, + "name": "Text-Tabs+Wrap License", + "licenseId": "TTWL", "seeAlso": [ - "http://old.zope.org/Resources/License/ZPL-2.0", - "https://opensource.org/licenses/ZPL-2.0" + "https://fedoraproject.org/wiki/Licensing/TTWL", + "https://github.com/ap/Text-Tabs/blob/master/lib.modern/Text/Tabs.pm#L148" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/copyleft-next-0.3.1.html", + "reference": "https://spdx.org/licenses/TU-Berlin-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/copyleft-next-0.3.1.json", - "referenceNumber": 442, - "name": "copyleft-next 0.3.1", - "licenseId": "copyleft-next-0.3.1", + "detailsUrl": "https://spdx.org/licenses/TU-Berlin-1.0.json", + "referenceNumber": 417, + "name": "Technische Universitaet Berlin License 1.0", + "licenseId": "TU-Berlin-1.0", "seeAlso": [ - "https://github.com/copyleft-next/copyleft-next/blob/master/Releases/copyleft-next-0.3.1" + "https://github.com/swh/ladspa/blob/7bf6f3799fdba70fda297c2d8fd9f526803d9680/gsm/COPYRIGHT" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GFDL-1.3-invariants-or-later.html", + "reference": "https://spdx.org/licenses/TU-Berlin-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.3-invariants-or-later.json", - "referenceNumber": 443, - "name": "GNU Free Documentation License v1.3 or later - invariants", - "licenseId": "GFDL-1.3-invariants-or-later", + "detailsUrl": "https://spdx.org/licenses/TU-Berlin-2.0.json", + "referenceNumber": 61, + "name": "Technische Universitaet Berlin License 2.0", + "licenseId": "TU-Berlin-2.0", "seeAlso": [ - "https://www.gnu.org/licenses/fdl-1.3.txt" + "https://github.com/CorsixTH/deps/blob/fd339a9f526d1d9c9f01ccf39e438a015da50035/licences/libgsm.txt" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/LPL-1.02.html", + "reference": "https://spdx.org/licenses/UCL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LPL-1.02.json", - "referenceNumber": 444, - "name": "Lucent Public License v1.02", - "licenseId": "LPL-1.02", + "detailsUrl": "https://spdx.org/licenses/UCL-1.0.json", + "referenceNumber": 201, + "name": "Upstream Compatibility License v1.0", + "licenseId": "UCL-1.0", "seeAlso": [ - "http://plan9.bell-labs.com/plan9/license.html", - "https://opensource.org/licenses/LPL-1.02" + "https://opensource.org/licenses/UCL-1.0" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/NLOD-1.0.html", + "reference": "https://spdx.org/licenses/Unicode-DFS-2015.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NLOD-1.0.json", - "referenceNumber": 445, - "name": "Norwegian Licence for Open Government Data (NLOD) 1.0", - "licenseId": "NLOD-1.0", + "detailsUrl": "https://spdx.org/licenses/Unicode-DFS-2015.json", + "referenceNumber": 332, + "name": "Unicode License Agreement - Data Files and Software (2015)", + "licenseId": "Unicode-DFS-2015", "seeAlso": [ - "http://data.norge.no/nlod/en/1.0" + "https://web.archive.org/web/20151224134844/http://unicode.org/copyright.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/MIT-Modern-Variant.html", + "reference": "https://spdx.org/licenses/Unicode-DFS-2016.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MIT-Modern-Variant.json", - "referenceNumber": 446, - "name": "MIT License Modern Variant", - "licenseId": "MIT-Modern-Variant", + "detailsUrl": "https://spdx.org/licenses/Unicode-DFS-2016.json", + "referenceNumber": 134, + "name": "Unicode License Agreement - Data Files and Software (2016)", + "licenseId": "Unicode-DFS-2016", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing:MIT#Modern_Variants", - "https://ptolemy.berkeley.edu/copyright.htm", - "https://pirlwww.lpl.arizona.edu/resources/guide/software/PerlTk/Tixlic.html" + "http://www.unicode.org/copyright.html" ], "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/MTLL.html", + "reference": "https://spdx.org/licenses/Unicode-TOU.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MTLL.json", - "referenceNumber": 447, - "name": "Matrix Template Library License", - "licenseId": "MTLL", + "detailsUrl": "https://spdx.org/licenses/Unicode-TOU.json", + "referenceNumber": 459, + "name": "Unicode Terms of Use", + "licenseId": "Unicode-TOU", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Matrix_Template_Library_License" + "http://www.unicode.org/copyright.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/ECL-1.0.html", + "reference": "https://spdx.org/licenses/Unlicense.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/ECL-1.0.json", - "referenceNumber": 448, - "name": "Educational Community License v1.0", - "licenseId": "ECL-1.0", + "detailsUrl": "https://spdx.org/licenses/Unlicense.json", + "referenceNumber": 194, + "name": "The Unlicense", + "licenseId": "Unlicense", "seeAlso": [ - "https://opensource.org/licenses/ECL-1.0" + "https://unlicense.org/" ], - "isOsiApproved": true + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/EPICS.html", + "reference": "https://spdx.org/licenses/UPL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/EPICS.json", - "referenceNumber": 449, - "name": "EPICS Open License", - "licenseId": "EPICS", + "detailsUrl": "https://spdx.org/licenses/UPL-1.0.json", + "referenceNumber": 443, + "name": "Universal Permissive License v1.0", + "licenseId": "UPL-1.0", "seeAlso": [ - "https://epics.anl.gov/license/open.php" + "https://opensource.org/licenses/UPL" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/BSD-3-Clause-Attribution.html", + "reference": "https://spdx.org/licenses/Vim.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/BSD-3-Clause-Attribution.json", - "referenceNumber": 450, - "name": "BSD with attribution", - "licenseId": "BSD-3-Clause-Attribution", + "detailsUrl": "https://spdx.org/licenses/Vim.json", + "referenceNumber": 128, + "name": "Vim License", + "licenseId": "Vim", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/BSD_with_Attribution" + "http://vimdoc.sourceforge.net/htmldoc/uganda.html" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/GFDL-1.2-invariants-only.html", + "reference": "https://spdx.org/licenses/VOSTROM.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GFDL-1.2-invariants-only.json", - "referenceNumber": 451, - "name": "GNU Free Documentation License v1.2 only - invariants", - "licenseId": "GFDL-1.2-invariants-only", + "detailsUrl": "https://spdx.org/licenses/VOSTROM.json", + "referenceNumber": 17, + "name": "VOSTROM Public License for Open Source", + "licenseId": "VOSTROM", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/fdl-1.2.txt" + "https://fedoraproject.org/wiki/Licensing/VOSTROM" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/AMDPLPA.html", + "reference": "https://spdx.org/licenses/VSL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/AMDPLPA.json", - "referenceNumber": 452, - "name": "AMD\u0027s plpa_map.c License", - "licenseId": "AMDPLPA", + "detailsUrl": "https://spdx.org/licenses/VSL-1.0.json", + "referenceNumber": 162, + "name": "Vovida Software License v1.0", + "licenseId": "VSL-1.0", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/AMD_plpa_map_License" + "https://opensource.org/licenses/VSL-1.0" ], - "isOsiApproved": false + "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/FSFUL.html", + "reference": "https://spdx.org/licenses/W3C.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/FSFUL.json", - "referenceNumber": 453, - "name": "FSF Unlimited License", - "licenseId": "FSFUL", + "detailsUrl": "https://spdx.org/licenses/W3C.json", + "referenceNumber": 149, + "name": "W3C Software Notice and License (2002-12-31)", + "licenseId": "W3C", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/FSF_Unlimited_License" + "http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231.html", + "https://opensource.org/licenses/W3C" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CC-BY-3.0-AT.html", + "reference": "https://spdx.org/licenses/W3C-19980720.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-3.0-AT.json", - "referenceNumber": 454, - "name": "Creative Commons Attribution 3.0 Austria", - "licenseId": "CC-BY-3.0-AT", + "detailsUrl": "https://spdx.org/licenses/W3C-19980720.json", + "referenceNumber": 315, + "name": "W3C Software Notice and License (1998-07-20)", + "licenseId": "W3C-19980720", "seeAlso": [ - "https://creativecommons.org/licenses/by/3.0/at/legalcode" + "http://www.w3.org/Consortium/Legal/copyright-software-19980720.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/CDLA-Sharing-1.0.html", + "reference": "https://spdx.org/licenses/W3C-20150513.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CDLA-Sharing-1.0.json", - "referenceNumber": 455, - "name": "Community Data License Agreement Sharing 1.0", - "licenseId": "CDLA-Sharing-1.0", + "detailsUrl": "https://spdx.org/licenses/W3C-20150513.json", + "referenceNumber": 226, + "name": "W3C Software Notice and Document License (2015-05-13)", + "licenseId": "W3C-20150513", "seeAlso": [ - "https://cdla.io/sharing-1-0" + "https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OLDAP-2.6.html", + "reference": "https://spdx.org/licenses/Watcom-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OLDAP-2.6.json", - "referenceNumber": 456, - "name": "Open LDAP Public License v2.6", - "licenseId": "OLDAP-2.6", + "detailsUrl": "https://spdx.org/licenses/Watcom-1.0.json", + "referenceNumber": 227, + "name": "Sybase Open Watcom Public License 1.0", + "licenseId": "Watcom-1.0", "seeAlso": [ - "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d1cae062821881f41b73012ba816434897abf4205" + "https://opensource.org/licenses/Watcom-1.0" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": false }, { - "reference": "https://spdx.org/licenses/LAL-1.3.html", + "reference": "https://spdx.org/licenses/Wsuipa.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/LAL-1.3.json", - "referenceNumber": 457, - "name": "Licence Art Libre 1.3", - "licenseId": "LAL-1.3", + "detailsUrl": "https://spdx.org/licenses/Wsuipa.json", + "referenceNumber": 157, + "name": "Wsuipa License", + "licenseId": "Wsuipa", "seeAlso": [ - "https://artlibre.org/" + "https://fedoraproject.org/wiki/Licensing/Wsuipa" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/OPUBL-1.0.html", + "reference": "https://spdx.org/licenses/WTFPL.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OPUBL-1.0.json", - "referenceNumber": 458, - "name": "Open Publication License v1.0", - "licenseId": "OPUBL-1.0", + "detailsUrl": "https://spdx.org/licenses/WTFPL.json", + "referenceNumber": 16, + "name": "Do What The F*ck You Want To Public License", + "licenseId": "WTFPL", "seeAlso": [ - "http://opencontent.org/openpub/", - "https://www.debian.org/opl", - "https://www.ctan.org/license/opl" + "http://www.wtfpl.net/about/", + "http://sam.zoy.org/wtfpl/COPYING" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Bahyph.html", + "reference": "https://spdx.org/licenses/wxWindows.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "https://spdx.org/licenses/wxWindows.json", + "referenceNumber": 383, + "name": "wxWindows Library License", + "licenseId": "wxWindows", + "seeAlso": [ + "https://opensource.org/licenses/WXwindows" + ], + "isOsiApproved": true + }, + { + "reference": "https://spdx.org/licenses/X11.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Bahyph.json", - "referenceNumber": 459, - "name": "Bahyph License", - "licenseId": "Bahyph", + "detailsUrl": "https://spdx.org/licenses/X11.json", + "referenceNumber": 167, + "name": "X11 License", + "licenseId": "X11", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Bahyph" + "http://www.xfree86.org/3.3.6/COPYRIGHT2.html#3" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CC-BY-SA-3.0-AT.html", + "reference": "https://spdx.org/licenses/X11-distribute-modifications-variant.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CC-BY-SA-3.0-AT.json", - "referenceNumber": 460, - "name": "Creative Commons Attribution Share Alike 3.0 Austria", - "licenseId": "CC-BY-SA-3.0-AT", + "detailsUrl": "https://spdx.org/licenses/X11-distribute-modifications-variant.json", + "referenceNumber": 217, + "name": "X11 License Distribution Modification Variant", + "licenseId": "X11-distribute-modifications-variant", "seeAlso": [ - "https://creativecommons.org/licenses/by-sa/3.0/at/legalcode" + "https://github.com/mirror/ncurses/blob/master/COPYING" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/AFL-2.1.html", + "reference": "https://spdx.org/licenses/Xerox.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/AFL-2.1.json", - "referenceNumber": 461, - "name": "Academic Free License v2.1", - "licenseId": "AFL-2.1", + "detailsUrl": "https://spdx.org/licenses/Xerox.json", + "referenceNumber": 493, + "name": "Xerox License", + "licenseId": "Xerox", "seeAlso": [ - "http://opensource.linux-mirror.org/licenses/afl-2.1.txt" + "https://fedoraproject.org/wiki/Licensing/Xerox" ], - "isOsiApproved": true, - "isFsfLibre": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/NTP-0.html", + "reference": "https://spdx.org/licenses/XFree86-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NTP-0.json", - "referenceNumber": 462, - "name": "NTP No Attribution", - "licenseId": "NTP-0", + "detailsUrl": "https://spdx.org/licenses/XFree86-1.1.json", + "referenceNumber": 378, + "name": "XFree86 License 1.1", + "licenseId": "XFree86-1.1", "seeAlso": [ - "https://github.com/tytso/e2fsprogs/blob/master/lib/et/et_name.c" + "http://www.xfree86.org/current/LICENSE4.html" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/OSL-2.1.html", + "reference": "https://spdx.org/licenses/xinetd.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OSL-2.1.json", - "referenceNumber": 463, - "name": "Open Software License 2.1", - "licenseId": "OSL-2.1", + "detailsUrl": "https://spdx.org/licenses/xinetd.json", + "referenceNumber": 343, + "name": "xinetd License", + "licenseId": "xinetd", "seeAlso": [ - "http://web.archive.org/web/20050212003940/http://www.rosenlaw.com/osl21.htm", - "https://opensource.org/licenses/OSL-2.1" + "https://fedoraproject.org/wiki/Licensing/Xinetd_License" ], - "isOsiApproved": true, + "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/LGPL-2.0.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/LGPL-2.0.json", - "referenceNumber": 464, - "name": "GNU Library General Public License v2 only", - "licenseId": "LGPL-2.0", + "reference": "https://spdx.org/licenses/Xnet.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/Xnet.json", + "referenceNumber": 82, + "name": "X.Net License", + "licenseId": "Xnet", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/lgpl-2.0-standalone.html" + "https://opensource.org/licenses/Xnet" ], "isOsiApproved": true }, { - "reference": "https://spdx.org/licenses/NCGL-UK-2.0.html", + "reference": "https://spdx.org/licenses/xpp.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/NCGL-UK-2.0.json", - "referenceNumber": 465, - "name": "Non-Commercial Government Licence", - "licenseId": "NCGL-UK-2.0", + "detailsUrl": "https://spdx.org/licenses/xpp.json", + "referenceNumber": 425, + "name": "XPP License", + "licenseId": "xpp", "seeAlso": [ - "http://www.nationalarchives.gov.uk/doc/non-commercial-government-licence/version/2/" + "https://fedoraproject.org/wiki/Licensing/xpp" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Glulxe.html", + "reference": "https://spdx.org/licenses/XSkat.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Glulxe.json", - "referenceNumber": 466, - "name": "Glulxe License", - "licenseId": "Glulxe", + "detailsUrl": "https://spdx.org/licenses/XSkat.json", + "referenceNumber": 146, + "name": "XSkat License", + "licenseId": "XSkat", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/Glulxe" + "https://fedoraproject.org/wiki/Licensing/XSkat_License" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/copyleft-next-0.3.0.html", + "reference": "https://spdx.org/licenses/YPL-1.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/copyleft-next-0.3.0.json", - "referenceNumber": 467, - "name": "copyleft-next 0.3.0", - "licenseId": "copyleft-next-0.3.0", + "detailsUrl": "https://spdx.org/licenses/YPL-1.0.json", + "referenceNumber": 318, + "name": "Yahoo! Public License v1.0", + "licenseId": "YPL-1.0", "seeAlso": [ - "https://github.com/copyleft-next/copyleft-next/blob/master/Releases/copyleft-next-0.3.0" + "http://www.zimbra.com/license/yahoo_public_license_1.0.html" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/dvipdfm.html", + "reference": "https://spdx.org/licenses/YPL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/dvipdfm.json", - "referenceNumber": 468, - "name": "dvipdfm License", - "licenseId": "dvipdfm", + "detailsUrl": "https://spdx.org/licenses/YPL-1.1.json", + "referenceNumber": 376, + "name": "Yahoo! Public License v1.1", + "licenseId": "YPL-1.1", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/dvipdfm" + "http://www.zimbra.com/license/yahoo_public_license_1.1.html" ], - "isOsiApproved": false + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/libtiff.html", + "reference": "https://spdx.org/licenses/Zed.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/libtiff.json", - "referenceNumber": 469, - "name": "libtiff License", - "licenseId": "libtiff", + "detailsUrl": "https://spdx.org/licenses/Zed.json", + "referenceNumber": 152, + "name": "Zed License", + "licenseId": "Zed", "seeAlso": [ - "https://fedoraproject.org/wiki/Licensing/libtiff" + "https://fedoraproject.org/wiki/Licensing/Zed" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/GLWTPL.html", + "reference": "https://spdx.org/licenses/Zend-2.0.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/GLWTPL.json", - "referenceNumber": 470, - "name": "Good Luck With That Public License", - "licenseId": "GLWTPL", - "seeAlso": [ - "https://github.com/me-shaon/GLWTPL/commit/da5f6bc734095efbacb442c0b31e33a65b9d6e85" - ], - "isOsiApproved": false - }, - { - "reference": "https://spdx.org/licenses/StandardML-NJ.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/StandardML-NJ.json", - "referenceNumber": 471, - "name": "Standard ML of New Jersey License", - "licenseId": "StandardML-NJ", + "detailsUrl": "https://spdx.org/licenses/Zend-2.0.json", + "referenceNumber": 60, + "name": "Zend License v2.0", + "licenseId": "Zend-2.0", "seeAlso": [ - "http://www.smlnj.org//license.html" + "https://web.archive.org/web/20130517195954/http://www.zend.com/license/2_00.txt" ], "isOsiApproved": false, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/CAL-1.0.html", + "reference": "https://spdx.org/licenses/Zimbra-1.3.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/CAL-1.0.json", - "referenceNumber": 472, - "name": "Cryptographic Autonomy License 1.0", - "licenseId": "CAL-1.0", + "detailsUrl": "https://spdx.org/licenses/Zimbra-1.3.json", + "referenceNumber": 282, + "name": "Zimbra Public License v1.3", + "licenseId": "Zimbra-1.3", "seeAlso": [ - "http://cryptographicautonomylicense.com/license-text.html", - "https://opensource.org/licenses/CAL-1.0" + "http://web.archive.org/web/20100302225219/http://www.zimbra.com/license/zimbra-public-license-1-3.html" ], - "isOsiApproved": true + "isOsiApproved": false, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/Artistic-1.0-Perl.html", + "reference": "https://spdx.org/licenses/Zimbra-1.4.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Artistic-1.0-Perl.json", - "referenceNumber": 473, - "name": "Artistic License 1.0 (Perl)", - "licenseId": "Artistic-1.0-Perl", + "detailsUrl": "https://spdx.org/licenses/Zimbra-1.4.json", + "referenceNumber": 424, + "name": "Zimbra Public License v1.4", + "licenseId": "Zimbra-1.4", "seeAlso": [ - "http://dev.perl.org/licenses/artistic.html" + "http://www.zimbra.com/legal/zimbra-public-license-1-4" ], - "isOsiApproved": true + "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/gSOAP-1.3b.html", + "reference": "https://spdx.org/licenses/Zlib.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/gSOAP-1.3b.json", - "referenceNumber": 474, - "name": "gSOAP Public License v1.3b", - "licenseId": "gSOAP-1.3b", + "detailsUrl": "https://spdx.org/licenses/Zlib.json", + "referenceNumber": 205, + "name": "zlib License", + "licenseId": "Zlib", "seeAlso": [ - "http://www.cs.fsu.edu/~engelen/license.html" + "http://www.zlib.net/zlib_license.html", + "https://opensource.org/licenses/Zlib" ], - "isOsiApproved": false + "isOsiApproved": true, + "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/OLDAP-2.5.html", + "reference": "https://spdx.org/licenses/zlib-acknowledgement.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/OLDAP-2.5.json", - "referenceNumber": 475, - "name": "Open LDAP Public License v2.5", - "licenseId": "OLDAP-2.5", + "detailsUrl": "https://spdx.org/licenses/zlib-acknowledgement.json", + "referenceNumber": 84, + "name": "zlib/libpng License with Acknowledgement", + "licenseId": "zlib-acknowledgement", "seeAlso": [ - "http://www.openldap.org/devel/gitweb.cgi?p\u003dopenldap.git;a\u003dblob;f\u003dLICENSE;hb\u003d6852b9d90022e8593c98205413380536b1b5a7cf" + "https://fedoraproject.org/wiki/Licensing/ZlibWithAcknowledgement" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/Interbase-1.0.html", + "reference": "https://spdx.org/licenses/ZPL-1.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/Interbase-1.0.json", - "referenceNumber": 476, - "name": "Interbase Public License v1.0", - "licenseId": "Interbase-1.0", + "detailsUrl": "https://spdx.org/licenses/ZPL-1.1.json", + "referenceNumber": 63, + "name": "Zope Public License 1.1", + "licenseId": "ZPL-1.1", "seeAlso": [ - "https://web.archive.org/web/20060319014854/http://info.borland.com/devsupport/interbase/opensource/IPL.html" + "http://old.zope.org/Resources/License/ZPL-1.1" ], "isOsiApproved": false }, { - "reference": "https://spdx.org/licenses/LGPL-2.1.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/LGPL-2.1.json", - "referenceNumber": 477, - "name": "GNU Lesser General Public License v2.1 only", - "licenseId": "LGPL-2.1", + "reference": "https://spdx.org/licenses/ZPL-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "https://spdx.org/licenses/ZPL-2.0.json", + "referenceNumber": 116, + "name": "Zope Public License 2.0", + "licenseId": "ZPL-2.0", "seeAlso": [ - "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", - "https://opensource.org/licenses/LGPL-2.1" + "http://old.zope.org/Resources/License/ZPL-2.0", + "https://opensource.org/licenses/ZPL-2.0" ], "isOsiApproved": true, "isFsfLibre": true }, { - "reference": "https://spdx.org/licenses/MS-PL.html", + "reference": "https://spdx.org/licenses/ZPL-2.1.html", "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MS-PL.json", - "referenceNumber": 478, - "name": "Microsoft Public License", - "licenseId": "MS-PL", + "detailsUrl": "https://spdx.org/licenses/ZPL-2.1.json", + "referenceNumber": 398, + "name": "Zope Public License 2.1", + "licenseId": "ZPL-2.1", "seeAlso": [ - "http://www.microsoft.com/opensource/licenses.mspx", - "https://opensource.org/licenses/MS-PL" + "http://old.zope.org/Resources/ZPL/" ], "isOsiApproved": true, "isFsfLibre": true } ], - "releaseDate": "2021-11-19" + "releaseDate": "2023-01-02" } \ No newline at end of file diff --git a/src/scanoss/delta.py b/src/scanoss/delta.py new file mode 100644 index 00000000..d3de7b07 --- /dev/null +++ b/src/scanoss/delta.py @@ -0,0 +1,197 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" +import os +import shutil +import tempfile +from typing import Optional + +from .scanossbase import ScanossBase + + +class Delta(ScanossBase): + """ + Handle delta scan operations by copying files into a dedicated delta directory. + + This class manages the creation of delta directories and copying of specified files + while preserving the directory structure. Files are read from an input file where each + line contains a file path to copy. + """ + + def __init__( # noqa: PLR0913 + self, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + filepath: str = None, + folder: str = None, + output: str = None, + root_dir: str = None, + ): + """ + Initialise the Delta instance. + + :param debug: Enable debug logging. + :param trace: Enable trace logging. + :param quiet: Enable quiet mode (suppress non-essential output). + :param filepath: Path to an input file containing a list of files to copy. + :param folder: A target delta directory path (auto-generated if not provided). + :param output: Output file path for the delta directory location (stdout if not provided). + """ + super().__init__(debug, trace, quiet) + self.filepath = filepath + self.folder = folder + self.output = output + self.root_dir = root_dir if root_dir else '.' + + def copy(self, input_file: str = None): + """ + Copy files listed in the input file to the delta directory. + + Reads the input file line by line, where each line contains a file path. + Creates the delta directory if it doesn't exist, then copies each file + while preserving its directory structure. + + :return: Tuple of (status_code, folder_path) where status_code is 0 for success, + 1 for error, and folder_path is the delta directory path + """ + input_file = input_file if input_file else self.filepath + if not input_file: + self.print_stderr('ERROR: No input file specified') + return 1, '' + # Validate that an input file exists + if not os.path.isfile(input_file): + self.print_stderr(f'ERROR: Input file {input_file} does not exist or is not a file') + return 1, '' + # Load the input file and validate it contains valid file paths + files = self.load_input_file(input_file) + if files is None: + return 1, '' + # Create delta dir (folder) + delta_folder = self.create_delta_dir(self.folder, self.root_dir) + if not delta_folder: + return 1, '' + # Print delta folder location to output + self.print_to_file_or_stdout(delta_folder, self.output) + # Process each file and copy it to the delta dir + for source_file in files: + # Normalise the source path to handle ".." and redundant separators + normalised_source = os.path.normpath(source_file) + if '..' in normalised_source: + self.print_stderr(f'WARNING: Source path escapes root directory for {source_file}. Skipping.') + continue + # Resolve to the absolute path for source validation + abs_source = os.path.abspath(os.path.join(self.root_dir, normalised_source)) + # Check if the source file exists and is a file + if not os.path.exists(abs_source) or not os.path.isfile(abs_source): + self.print_stderr(f'WARNING: File {source_file} does not exist or is not a file, skipping') + continue + # Use a normalised source for destination to prevent traversal + dest_path = os.path.normpath(os.path.join(self.root_dir, delta_folder, normalised_source.lstrip(os.sep))) + # Final safety check: ensure destination is within the delta folder + abs_dest = os.path.abspath(dest_path) + abs_folder = os.path.abspath(os.path.join(self.root_dir, delta_folder)) + if not abs_dest.startswith(abs_folder + os.sep): + self.print_stderr( + f'WARNING: Destination path ({abs_dest}) escapes delta directory for {source_file}. Skipping.') + continue + # Create the destination directory if it doesn't exist and copy the file + try: + dest_dir = os.path.dirname(dest_path) + if dest_dir: + self.print_trace(f'Creating directory {dest_dir}...') + os.makedirs(dest_dir, exist_ok=True) + self.print_debug(f'Copying {source_file} to {dest_path} ...') + shutil.copy(abs_source, dest_path) + except (OSError, shutil.Error) as e: + self.print_stderr(f'ERROR: Failed to copy {source_file} to {dest_path}: {e}') + return 1, '' + return 0, delta_folder + + def create_delta_dir(self, folder: str, root_dir: str = '.') -> str or None: + """ + Create the delta directory. + + If no folder is specified, creates a unique temporary directory with + a 'delta-' prefix in the current directory. If a folder is specified, + validates that it doesn't already exist before creating it. + + :param root_dir: Root directory to create the delta directory in (default: current directory) + :param folder: Optional target directory + :return: Path to the delta directory, or None if it already exists or creation fails + """ + if folder: + # Resolve a relative folder under root_dir so checks/creation apply to the right place + resolved = folder if os.path.isabs(folder) else os.path.join(root_dir, folder) + resolved = os.path.normpath(resolved) + # Validate the target directory doesn't already exist and create it + if os.path.exists(resolved): + self.print_stderr(f'ERROR: Folder {resolved} already exists.') + return None + else: + try: + self.print_debug(f'Creating delta directory {resolved}...') + os.makedirs(resolved) + except (OSError, IOError) as e: + self.print_stderr(f'ERROR: Failed to create directory {resolved}: {e}') + return None + else: + # Create a unique temporary directory in the given root directory + try: + self.print_debug(f'Creating temporary delta directory in {root_dir} ...') + folder = tempfile.mkdtemp(prefix="delta-", dir=root_dir) + if folder: + folder = os.path.relpath(folder, start=root_dir) # Get the relative path from root_dir + self.print_debug(f'Created temporary delta directory: {folder}') + except (OSError, IOError) as e: + self.print_stderr(f'ERROR: Failed to create temporary directory in {root_dir}: {e}') + return None + return folder + + def load_input_file(self, input_file: str) -> Optional[list[str]]: + """ + Loads and parses the input file line by line. Each line in the input + file represents a source file path, which will be stripped of trailing + whitespace and appended to the resulting list if it is not empty. + + :param input_file: The path to the input file to be read. + :type input_file: String + :return: A list of source file paths extracted from the input file, + or None if an error occurs or the file path is invalid. + :rtype: An array list[str] or None + """ + files = [] + if input_file: + try: + with open(input_file, 'r', encoding='utf-8') as f: + for line in f: + source_file = line.rstrip() + if source_file: + # Save the file path without any leading separators + files.append(source_file.lstrip(os.sep)) + # End of for loop + except (OSError, IOError) as e: + self.print_stderr(f'ERROR: Failed to read input file; {input_file}: {e}') + return None + self.print_debug(f'Loaded {len(files)} files from input file.') + return files diff --git a/src/scanoss/export/__init__.py b/src/scanoss/export/__init__.py new file mode 100644 index 00000000..1e95c46d --- /dev/null +++ b/src/scanoss/export/__init__.py @@ -0,0 +1,23 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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/src/scanoss/export/dependency_track.py b/src/scanoss/export/dependency_track.py new file mode 100644 index 00000000..f3969f40 --- /dev/null +++ b/src/scanoss/export/dependency_track.py @@ -0,0 +1,227 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +import base64 +import json +import traceback + +import requests + +from ..cyclonedx import CycloneDx +from ..scanossbase import ScanossBase +from ..services.dependency_track_service import DependencyTrackService +from ..utils.file import validate_json_file + + +def _build_payload(encoded_sbom: str, project_id, project_name, project_version) -> dict: + """ + Build the API payload + + Args: + encoded_sbom: Base64 encoded SBOM + + Returns: + API payload dictionary + """ + if project_id: + return {'project': project_id, 'bom': encoded_sbom} + else: + return { + 'projectName': project_name, + 'projectVersion': project_version, + 'autoCreate': True, + 'bom': encoded_sbom, + } + + +class DependencyTrackExporter(ScanossBase): + """ + Class for exporting SBOM files to Dependency Track + """ + def __init__( # noqa: PLR0913 + self, + url: str = None, + apikey: str = None, + output: str = None, + debug: bool = False, + trace: bool = False, + quiet: bool = False + ): + """ + Initialize DependencyTrackExporter + + Args: + url: Dependency Track URL + apikey: Dependency Track API Key + output: File to store output response data (optional) + debug: Enable debug output + trace: Enable trace output + quiet: Enable quiet mode + """ + super().__init__(debug=debug, trace=trace, quiet=quiet) + self.url = url.rstrip('/') + self.apikey = apikey + self.output = output + self.dt_service = DependencyTrackService(self.apikey, self.url, debug=debug, trace=trace, quiet=quiet) + + def _read_and_validate_sbom(self, input_file: str) -> dict: + """ + Read and validate the SBOM file + + Args: + input_file: Path to the SBOM file + + Returns: + Parsed SBOM content as dictionary + + Raises: + ValueError: If the file doesn't exist or is invalid or not a valid CycloneDX SBOM + """ + result = validate_json_file(input_file) + if not result.is_valid: + raise ValueError(f'Invalid JSON file: {result.error}') + + cdx = CycloneDx(debug=self.debug) + if not cdx.is_cyclonedx_json(json.dumps(result.data)): + raise ValueError(f'Input file is not a valid CycloneDX SBOM: {input_file}') + return result.data + + def _encode_sbom(self, sbom_content: dict) -> str: + """ + Encode SBOM content to base64 + + Args: + sbom_content: SBOM dictionary + + Returns: + Base64 encoded string + """ + if not sbom_content: + self.print_stderr('Warning: Empty SBOM content provided') + return '' + # Check if SBOM has no components (empty scan results) + components = sbom_content.get('components', []) + if len(components) == 0: + self.print_msg('Notice: SBOM contains no components (empty scan results)') + json_str = json.dumps(sbom_content, separators=(',', ':')) + encoded = base64.b64encode(json_str.encode('utf-8')).decode('utf-8') + return encoded + + def upload_sbom_file(self, input_file, project_id, project_name, project_version, output_file): + """ + Uploads an SBOM file to the specified project with an + optional output file and processes the file content for validation. + + Args: + input_file (str): The path to the SBOM file to be read and uploaded. + project_id (str): The unique identifier of the project to which the SBOM is being uploaded. + project_name (str): The name of the project to which the SBOM is being uploaded. + project_version (str): The version of the project to which the SBOM is being uploaded. + output_file (str): The path to save output related to the SBOM upload process. + + Returns: + bool: Returns True if the SBOM file was uploaded successfully, False otherwise. + + Raises: + ValueError: Raised if there are validation issues with the SBOM content. + """ + try: + if not self.quiet: + self.print_stderr(f'Reading SBOM file: {input_file}') + sbom_content = self._read_and_validate_sbom(input_file) + return self.upload_sbom_contents(sbom_content, project_id, project_name, project_version, output_file) + except ValueError as e: + self.print_stderr(f'Validation error: {e}') + return False + + def upload_sbom_contents(self, sbom_content: dict, project_id, project_name, project_version, output_file) -> bool: + """ + Uploads an SBOM to a Dependency Track server. + + Parameters: + sbom_content (dict): The SBOM content in dictionary format to be uploaded. + project_id: The unique identifier for the project. + project_name: The name of the project in Dependency Track. + project_version: The version of the project in Dependency Track. + output_file: The path to the file where the token and UUID data + should be written. If not provided, the data will be written to + standard output. + + Returns: + bool: True if the upload is successful; False otherwise. + + Raises: + ValueError: If the SBOM encoding process fails. + requests.exceptions.RequestException: If an error occurs during the HTTP request. + Exception: For any other unexpected error. + """ + if not project_id and not (project_name and project_version): + self.print_stderr('Error: Missing project id or name and version.') + return False + output = self.output + if output_file: + output = output_file + try: + self.print_debug('Encoding SBOM to base64') + payload = _build_payload(self._encode_sbom(sbom_content), project_id, project_name, project_version) + url = f'{self.url}/api/v1/bom' + headers = {'Content-Type': 'application/json', 'X-Api-Key': self.apikey} + self.print_trace(f'URL: {url}, Headers: {headers}, Payload keys: {list(payload.keys())}') + self.print_msg('Uploading SBOM to Dependency Track...') + response = requests.put(url, json=payload, headers=headers) + response.raise_for_status() + # Treat any 2xx status as success + if (requests.codes.ok <= response.status_code < requests.codes.multiple_choices and + response.status_code != requests.codes.no_content): + self.print_msg('SBOM uploaded successfully') + try: + response_data = response.json() + token = '' + project_uuid = project_id + if 'token' in response_data: + token = response_data['token'] + if project_name and project_version: + project_data = self.dt_service.get_project_by_name_version(project_name, project_version) + if project_data: + project_uuid = project_data.get("uuid", project_id) + token_json = json.dumps( + {"token": token, "project_uuid": project_uuid}, + indent=2 + ) + self.print_to_file_or_stdout(token_json, output) + except json.JSONDecodeError: + pass + return True + else: + self.print_stderr(f'Upload failed with status code: {response.status_code}') + self.print_stderr(f'Response: {response.text}') + except ValueError as e: + self.print_stderr(f'DT SBOM Upload Validation error: {e}') + except requests.exceptions.RequestException as e: + self.print_stderr(f'DT API Request error: {e}') + except Exception as e: + self.print_stderr(f'Unexpected error: {e}') + if self.debug: + traceback.print_exc() + return False diff --git a/src/scanoss/file_filters.py b/src/scanoss/file_filters.py new file mode 100644 index 00000000..33595374 --- /dev/null +++ b/src/scanoss/file_filters.py @@ -0,0 +1,582 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2024, SCANOSS + + 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. +""" + +import os +import sys +from pathlib import Path +from typing import List, Optional + +from pathspec import GitIgnoreSpec + +from .scanossbase import ScanossBase + +# Files to skip +DEFAULT_SKIPPED_FILES = { + 'gradlew', + 'gradlew.bat', + 'mvnw', + 'mvnw.cmd', + 'gradle-wrapper.jar', + 'maven-wrapper.jar', + 'thumbs.db', + 'babel.config.js', + 'license.txt', + 'license.md', + 'copying.lib', + 'makefile', +} + +DEFAULT_SKIPPED_FILES_HFH = { + 'gradlew', + 'gradlew.bat', + 'mvnw', + 'mvnw.cmd', + 'gradle-wrapper.jar', + 'maven-wrapper.jar', + 'thumbs.db', + 'babel.config.js', +} + + +# Folders to skip +DEFAULT_SKIPPED_DIRS = { + 'nbproject', + 'nbbuild', + 'nbdist', + '__pycache__', + 'venv', + '_yardoc', + 'eggs', + 'wheels', + 'htmlcov', + '__pypackages__', + 'example', + 'examples' +} + +DEFAULT_SKIPPED_DIRS_HFH = { + 'nbproject', + 'nbbuild', + 'nbdist', + '__pycache__', + 'venv', + '_yardoc', + 'eggs', + 'wheels', + 'htmlcov', + '__pypackages__', + 'example', + 'examples', +} + + +# Folder endings to skip +DEFAULT_SKIPPED_DIR_EXT = {'.egg-info'} +DEFAULT_SKIPPED_DIR_EXT_HFH = {'.egg-info'} + +# File extensions to skip +DEFAULT_SKIPPED_EXT = { + '.1', + '.2', + '.3', + '.4', + '.5', + '.6', + '.7', + '.8', + '.9', + '.ac', + '.adoc', + '.am', + '.asciidoc', + '.bmp', + '.build', + '.cfg', + '.chm', + '.class', + '.cmake', + '.cnf', + '.conf', + '.config', + '.contributors', + '.copying', + '.crt', + '.csproj', + '.css', + '.csv', + '.dat', + '.data', + '.doc', + '.docx', + '.dtd', + '.dts', + '.iws', + '.c9', + '.c9revisions', + '.dtsi', + '.dump', + '.eot', + '.eps', + '.geojson', + '.gdoc', + '.gif', + '.glif', + '.gmo', + '.gradle', + '.guess', + '.hex', + '.htm', + '.html', + '.ico', + '.iml', + '.in', + '.inc', + '.info', + '.ini', + '.ipynb', + '.jpeg', + '.jpg', + '.json', + '.jsonld', + '.lock', + '.log', + '.m4', + '.map', + '.markdown', + '.md', + '.md5', + '.meta', + '.mk', + '.mxml', + '.o', + '.otf', + '.out', + '.pbtxt', + '.pdf', + '.pem', + '.phtml', + '.plist', + '.png', + '.po', + '.ppt', + '.prefs', + '.properties', + '.pyc', + '.qdoc', + '.result', + '.rgb', + '.rst', + '.scss', + '.sha', + '.sha1', + '.sha2', + '.sha256', + '.sln', + '.spec', + '.sql', + '.sub', + '.svg', + '.svn-base', + '.tab', + '.template', + '.test', + '.tex', + '.tiff', + '.toml', + '.ttf', + '.txt', + '.utf-8', + '.vim', + '.wav', + '.woff', + '.woff2', + '.xht', + '.xhtml', + '.xls', + '.xlsx', + '.xml', + '.xpm', + '.xsd', + '.xul', + '.yaml', + '.yml', + '.wfp', + '.editorconfig', + '.dotcover', + '.pid', + '.lcov', + '.egg', + '.manifest', + '.cache', + '.coverage', + '.cover', + '.gem', + '.lst', + '.pickle', + '.pdb', + '.gml', + '.pot', + '.plt', + '.whml', + '.pom', + '.smtml', + '.min.js', + '.mf', + '.base64', + '.s', + '.diff', + '.patch', + '.rules', + # File endings + '-doc', + 'changelog', + 'config', + 'copying', + 'license', + 'authors', + 'news', + 'licenses', + 'notice', + 'readme', + 'swiftdoc', + 'texidoc', + 'todo', + 'version', + 'ignore', + 'manifest', + 'sqlite', + 'sqlite3', +} + + +class FileFilters(ScanossBase): + """ + Filter for determining which files to process during scanning, fingerprinting, etc. + Handles both inclusion and exclusion rules based on file paths, extensions, and sizes. + """ + + def __init__(self, debug: bool = False, trace: bool = False, quiet: bool = False, **kwargs): + """ + Initialize scan filters based on default settings. Optionally append custom settings. + + Args: + debug (bool): Enable debug output + trace (bool): Enable trace output + quiet (bool): Suppress output + **kwargs: Additional arguments including: + scanoss_settings (ScanossSettings): Custom settings to override defaults + all_extensions (bool): Include all file extensions + all_folders (bool): Include all folders + hidden_files_folders (bool): Include hidden files and folders + operation_type (str): Operation type ('scanning' or 'fingerprinting') + skip_size (int): Size to skip + skip_extensions (list): Extensions to skip + skip_folders (list): Folders to skip + is_folder_hashing_scan (bool): Whether the operation is a folder hashing scan + """ + super().__init__(debug, trace, quiet) + + self.hidden_files_folders = kwargs.get('hidden_files_folders', False) + self.scanoss_settings = kwargs.get('scanoss_settings') + self.all_extensions = kwargs.get('all_extensions', False) + self.all_folders = kwargs.get('all_folders', False) + self.skip_folders = kwargs.get('skip_folders', []) + self.skip_size = kwargs.get('skip_size', 0) + self.skip_extensions = kwargs.get('skip_extensions', []) + self.is_folder_hashing_scan = kwargs.get('is_folder_hashing_scan', False) + self.file_folder_pat_spec = self._get_file_folder_pattern_spec(kwargs.get('operation_type', 'scanning')) + self.size_pat_rules = self._get_size_limit_pattern_rules(kwargs.get('operation_type', 'scanning')) + + def get_filtered_files_from_folder(self, root: str) -> List[str]: + """ + Retrieve a list of files to scan or fingerprint from a given directory root based on filter settings. + + Args: + root (str): Root directory to scan or fingerprint + + Returns: + list[str]: Filtered list of files to scan or fingerprint + """ + if self.debug: + if self.file_folder_pat_spec: + self.print_stderr(f'Running with {len(self.file_folder_pat_spec)} pattern filters.') + if self.size_pat_rules: + self.print_stderr(f'Running with {len(self.size_pat_rules)} size pattern rules.') + if self.skip_size: + self.print_stderr(f'Running with global skip size: {self.skip_size}') + if self.skip_extensions: + self.print_stderr(f'Running with extra global skip extensions: {self.skip_extensions}') + if self.skip_folders: + self.print_stderr(f'Running with extra global skip folders: {self.skip_folders}') + all_files = [] + root_path = Path(root).resolve() + if not root_path.exists() or not root_path.is_dir(): + self.print_stderr(f'ERROR: Specified root directory {root} does not exist or is not a directory.') + return all_files + # Walk the tree looking for files to process. While taking into account files/folders to skip + for dirpath, dirnames, filenames in os.walk(root_path): + dir_path = Path(dirpath) + rel_path = dir_path.relative_to(root_path) + if dir_path.is_symlink(): # TODO should we skip symlink folders? + self.print_msg(f'WARNING: Found symbolic link folder: {dir_path}') + + if self.should_skip_dir(str(rel_path)): # Current directory should be skipped + dirnames.clear() + continue + for filename in filenames: + file_path = dir_path / filename + all_files.append(str(file_path)) + # End os.walk loop + # Now filter the files and return the reduced list + return self.get_filtered_files_from_files(all_files, str(root_path)) + + def get_filtered_files_from_files(self, files: List[str], scan_root: Optional[str] = None) -> List[str]: + """ + Retrieve a list of files to scan or fingerprint from a given list of files based on filter settings. + + Args: + files (List[str]): List of files to scan or fingerprint + scan_root (str): Root directory to scan or fingerprint + + Returns: + list[str]: Filtered list of files to scan or fingerprint + """ + filtered_files = [] + for file_path in files: + path_obj = Path(file_path) + try: + if scan_root: + rel_path = path_obj.relative_to(scan_root) + else: + rel_path = str(path_obj) + except ValueError: + self.print_debug(f'Ignoring file: {file_path} (broken symlink)') + continue + + if not path_obj.exists() or not path_obj.is_file() or path_obj.is_symlink(): + self.print_debug( + f'WARNING: File {rel_path} does not exist, is not a file, or is a symbolic link. Ignoring.' + ) + continue + + if not self.hidden_files_folders and any(part.startswith('.') for part in path_obj.parts): + self.print_debug(f'Skipping file: {rel_path} (in hidden directory or is hidden file)') + continue + + if self._should_skip_file(rel_path): + continue + try: + file_size = path_obj.stat().st_size + if file_size == 0: + self.print_debug(f'Skipping file: {rel_path} (empty file)') + continue + min_size, max_size = self._get_operation_size_limits(file_path) + if min_size <= file_size <= max_size: + filtered_files.append(str(rel_path)) + else: + self.print_debug( + f'Skipping file: {rel_path} (size {file_size} outside limits {min_size}-{max_size})' + ) + except OSError as e: + self.print_debug(f'Error getting size for {rel_path}: {e}') + # End file loop + return filtered_files + + def _get_file_folder_pattern_spec(self, operation_type: str = 'scanning'): + """ + Get file path pattern specification. + + Args: + operation_type (str): Type of operation ('scanning' or 'fingerprinting') + + Returns: + GitIgnoreSpec: GitIgnoreSpec object containing the file path patterns + """ + patterns = self._get_operation_patterns(operation_type) + if patterns: + return GitIgnoreSpec.from_lines(patterns) + return None + + def _get_size_limit_pattern_rules(self, operation_type: str = 'scanning'): + """ + Get size limit pattern rules. + + Args: + operation_type (str): Type of operation ('scanning' or 'fingerprinting') + + Returns: + List of size limit pattern rules + """ + if self.scanoss_settings: + size_rules = self.scanoss_settings.get_skip_sizes(operation_type) + if size_rules: + size_rules_with_patterns = [] + for rule in size_rules: + patterns = rule.get('patterns', []) + if not patterns: + continue + size_rules_with_patterns.append(rule) + return size_rules_with_patterns + return None + + def _get_operation_patterns(self, operation_type: str) -> List[str]: + """ + Get patterns specific to the operation type, combining defaults with settings. + + Args: + operation_type (str): Type of operation ('scanning' or 'fingerprinting') + + Returns: + List[str]: Combined list of patterns to skip + """ + patterns = [] + + # Default patterns for skipping directories + if not self.all_folders: + DEFAULT_SKIPPED_DIR_LIST = DEFAULT_SKIPPED_DIRS_HFH if self.is_folder_hashing_scan else DEFAULT_SKIPPED_DIRS + DEFAULT_SKIPPED_DIR_EXT_LIST = ( + DEFAULT_SKIPPED_DIR_EXT_HFH if self.is_folder_hashing_scan else DEFAULT_SKIPPED_DIR_EXT + ) + for dir_name in DEFAULT_SKIPPED_DIR_LIST: + patterns.append(f'{dir_name}/') + for dir_extension in DEFAULT_SKIPPED_DIR_EXT_LIST: + patterns.append(f'*{dir_extension}/') + + # Custom patterns added in SCANOSS settings file + if self.scanoss_settings: + patterns.extend(self.scanoss_settings.get_skip_patterns(operation_type)) + return patterns + + def _get_operation_size_limits(self, file_path: str = None) -> tuple: + """ + Get size limits specific to the operation type and file path. + + Args: + file_path (str, optional): Path to the file to check against patterns. If None, returns default limits. + + Returns: + tuple: (min_size, max_size) tuple for the given file path and operation type + """ + min_size = 0 + max_size = sys.maxsize + # Apply global minimum file size if specified + if self.skip_size > 0: + min_size = self.skip_size + return min_size, max_size + # Return default size limits if no settings specified + if not self.scanoss_settings or not file_path or not self.size_pat_rules: + return min_size, max_size + try: + rel_path = os.path.relpath(file_path) + except ValueError: + rel_path = os.path.basename(file_path) + rel_path_lower = rel_path.lower() + # Cycle through each rule looking for a match + for rule in self.size_pat_rules: + patterns = rule.get('patterns', []) + if patterns: + path_spec = GitIgnoreSpec.from_lines(patterns) + if path_spec.match_file(rel_path_lower): + return rule.get('min', min_size), rule.get('max', max_size) + # End rules loop + return min_size, max_size + + def should_skip_dir(self, dir_rel_path: str) -> bool: # noqa: PLR0911 + """ + Check if a directory should be skipped based on operation type and default rules. + + Args: + dir_rel_path (str): Relative path to the directory + + Returns: + bool: True if directory should be skipped, False otherwise + """ + dir_name = os.path.basename(dir_rel_path) + dir_path = Path(dir_rel_path) + if ( + not self.hidden_files_folders + and dir_path != Path('.') + and any(part.startswith('.') for part in dir_path.parts) + ): + self.print_debug(f'Skipping directory: {dir_rel_path} (hidden directory)') + return True + if self.all_folders: + return False + dir_name_lower = dir_name.lower() + if dir_name_lower in DEFAULT_SKIPPED_DIRS: + self.print_debug(f'Skipping directory: {dir_rel_path} (matches default skip directory)') + return True + if self.skip_folders and dir_name in self.skip_folders: + self.print_debug(f'Skipping directory: {dir_rel_path} (matches skip folder)') + return True + for ext in DEFAULT_SKIPPED_DIR_EXT: + if dir_name_lower.endswith(ext): + self.print_debug(f'Skipping directory: {dir_rel_path} (matches default skip extension: {ext})') + return True + + if self.file_folder_pat_spec and self.file_folder_pat_spec.match_file(dir_rel_path): + self.print_debug(f'Skipping directory: {dir_rel_path} (matches custom pattern)') + return True + return False + + def _should_skip_file(self, file_rel_path: str) -> bool: # noqa: PLR0911 + """ + Check if a file should be skipped based on operation type and default rules. + + Args: + file_rel_path (str): Relative path to the file + + Returns: + bool: True if file should be skipped, False otherwise + """ + file_name = os.path.basename(file_rel_path) + DEFAULT_SKIPPED_EXT_LIST = {} if self.is_folder_hashing_scan else DEFAULT_SKIPPED_EXT + DEFAULT_SKIPPED_FILES_LIST = DEFAULT_SKIPPED_FILES_HFH if self.is_folder_hashing_scan else DEFAULT_SKIPPED_FILES + + if not self.hidden_files_folders and file_name.startswith('.'): + self.print_debug(f'Skipping file: {file_rel_path} (hidden file)') + return True + if self.all_extensions: + return False + file_name_lower = file_name.lower() + # Look for exact files + if file_name_lower in DEFAULT_SKIPPED_FILES_LIST: + self.print_debug(f'Skipping file: {file_rel_path} (matches default skip file)') + return True + # Look for file endings + for ending in DEFAULT_SKIPPED_EXT_LIST: + if file_name_lower.endswith(ending): + self.print_debug(f'Skipping file: {file_rel_path} (matches default skip ending: {ending})') + return True + # Look for custom (extra) endings + if self.skip_extensions: + for ending in self.skip_extensions: + if file_name_lower.endswith(ending): + self.print_debug(f'Skipping file: {file_rel_path} (matches skip extension)') + return True + # Check for file patterns + if self.file_folder_pat_spec and self.file_folder_pat_spec.match_file(file_rel_path): + self.print_debug(f'Skipping file: {file_rel_path} (matches custom pattern)') + return True + return False diff --git a/src/scanoss/filecount.py b/src/scanoss/filecount.py new file mode 100644 index 00000000..87b8df75 --- /dev/null +++ b/src/scanoss/filecount.py @@ -0,0 +1,171 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2022, SCANOSS + + 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. +""" + +import csv +import os +import pathlib +import sys +from contextlib import nullcontext + +from progress.spinner import Spinner + +from .scanossbase import ScanossBase + + +class FileCount(ScanossBase): + """ + SCANOSS File Type Count class + Handle the scanning of files, snippets and dependencies + """ + + def __init__( + self, + scan_output: str = None, + hidden_files_folders: bool = False, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + ): + """ + Initialise scanning class + """ + super().__init__(debug, trace, quiet) + self.scan_output = scan_output + self.isatty = sys.stderr.isatty() + self.hidden_files_folders = hidden_files_folders + + def __filter_files(self, files: list) -> list: + """ + Filter which files should be considered for processing + :param files: list of files to filter + :return list of filtered files + """ + file_list = [] + for f in files: + ignore = False + if f.startswith('.') and not self.hidden_files_folders: # Ignore all . files unless requested + ignore = True + if not ignore: + file_list.append(f) + return file_list + + def __filter_dirs(self, dirs: list) -> list: + """ + Filter which folders should be considered for processing + :param dirs: list of directories to filter + :return: list of filtered directories + """ + dir_list = [] + for d in dirs: + ignore = False + if d.startswith('.') and not self.hidden_files_folders: # Ignore all . folders unless requested + ignore = True + if not ignore: + dir_list.append(d) + return dir_list + + def __log_result(self, string, outfile=None): + """ + Logs result to file or STDOUT + """ + if not outfile and self.scan_output: + outfile = self.scan_output + if outfile: + with open(outfile, 'a') as rf: + rf.write(string + '\n') + else: + print(string) + + def count_files(self, scan_dir: str) -> bool: + """ + Search the specified folder producing counting the file types found + :param scan_dir str + Directory to scan + :return True if successful, False otherwise + """ + success = True + if not scan_dir: + raise Exception('ERROR: Please specify a folder to scan') + if not os.path.exists(scan_dir) or not os.path.isdir(scan_dir): + raise Exception(f'ERROR: Specified folder does not exist or is not a folder: {scan_dir}') + + self.print_msg(f'Searching {scan_dir} for files to count...') + spinner_ctx = Spinner('Searching ') if (not self.quiet and self.isatty) else nullcontext() + + with spinner_ctx as spinner: + file_types = {} + file_count = 0 + file_size = 0 + for root, dirs, files in os.walk(scan_dir): + self.print_trace(f'U Root: {root}, Dirs: {dirs}, Files {files}') + dirs[:] = self.__filter_dirs(dirs) # Strip out unwanted directories + filtered_files = self.__filter_files(files) # Strip out unwanted files + self.print_trace(f'F Root: {root}, Dirs: {dirs}, Files {filtered_files}') + for file in filtered_files: # Cycle through each filtered file + path = os.path.join(root, file) + f_size = 0 + try: + f_size = os.stat(path).st_size + except Exception as e: + self.print_trace(f'Ignoring missing symlink file: {file} ({e})') # broken symlink + if f_size > 0: # Ignore broken links and empty files + file_count = file_count + 1 + file_size = file_size + f_size + f_suffix = pathlib.Path(file).suffix + if not f_suffix or f_suffix == '': + f_suffix = 'no_suffix' + self.print_trace(f'Counting {path} ({f_suffix} - {f_size})..') + fc = file_types.get(f_suffix) + if not fc: + fc = [1, f_size] + else: + fc[0] = fc[0] + 1 + fc[1] = fc[1] + f_size + file_types[f_suffix] = fc + if spinner: + spinner.next() + # End for loop + self.print_stderr(f'Found {file_count:,.0f} files with a total size of {file_size / (1 << 20):,.2f} MB.') + if file_types: + csv_dict = [] + for k in file_types: + d = file_types[k] + csv_dict.append({'extension': k, 'count': d[0], 'size(MB)': f'{d[1] / (1 << 20):,.2f}'}) + fields = ['extension', 'count', 'size(MB)'] + file = sys.stdout + if self.scan_output: + file = open(self.scan_output, 'w') + writer = csv.DictWriter(file, fieldnames=fields) + writer.writeheader() # writing headers (field names) + writer.writerows(csv_dict) # writing data rows + if self.scan_output: + file.close() + else: + FileCount.print_stderr(f'Warning: No files found to count in folder: {scan_dir}') + return success + + +# +# End of ScanOSS Class +# diff --git a/src/scanoss/gitlabqualityreport.py b/src/scanoss/gitlabqualityreport.py new file mode 100644 index 00000000..1a1b8ec6 --- /dev/null +++ b/src/scanoss/gitlabqualityreport.py @@ -0,0 +1,214 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +import json +import os +import sys +from dataclasses import dataclass + +from .scanossbase import ScanossBase +from .utils import scanoss_scan_results_utils + + +@dataclass +class Lines: + begin: int + +@dataclass +class Location: + path: str + lines: Lines + +@dataclass +class CodeQuality: + description: str + check_name: str + fingerprint: str + severity: str + location: Location + + def to_dict(self): + """Convert to dictionary for JSON serialization.""" + return { + "description": self.description, + "check_name": self.check_name, + "fingerprint": self.fingerprint, + "severity": self.severity, + "location": { + "path": self.location.path, + "lines": { + "begin": self.location.lines.begin + } + } + } + +class GitLabQualityReport(ScanossBase): + """ + GitLabCodeQuality management class + Handle all interaction with GitLab Code Quality Report formatting + """ + + def __init__(self, debug: bool = False, trace: bool = False, quiet: bool = False): + """ + Initialise the GitLabCodeQuality class + """ + super().__init__(debug, trace, quiet) + self.print_trace(f"GitLabQualityReport initialized with debug={debug}, trace={trace}, quiet={quiet}") + + + def _get_code_quality(self, file_name: str, result: dict) -> CodeQuality or None: + self.print_trace(f"_get_code_quality called for file: {file_name}") + self.print_trace(f"Processing result: {result}") + + if not result.get('file_hash'): + self.print_debug(f"Warning: no hash found for result: {result}") + return None + + if result.get('id') == 'file': + self.print_debug(f"Processing file match for: {file_name}") + description = f"File match found in: {file_name}" + code_quality = CodeQuality( + description=description, + check_name=file_name, + fingerprint=result.get('file_hash'), + severity="info", + location=Location( + path=file_name, + lines = Lines( + begin= 1 + ) + ) + ) + self.print_trace(f"Created file CodeQuality object: {code_quality}") + return code_quality + + if not result.get('lines'): + self.print_debug(f"Warning: No lines found for result: {result}") + return None + lines = scanoss_scan_results_utils.get_lines(result.get('lines')) + self.print_trace(f"Extracted lines: {lines}") + if len(lines) == 0: + self.print_debug(f"Warning: empty lines for result: {result}") + return None + end_line = lines[len(lines) - 1] if len(lines) > 1 else lines[0] + description = f"Snippet found in: {file_name} - lines {lines[0]}-{end_line}" + self.print_debug(f"Processing snippet match for: {file_name}, lines: {lines[0]}-{end_line}") + code_quality = CodeQuality( + description=description, + check_name=file_name, + fingerprint=result.get('file_hash'), + severity="info", + location=Location( + path=file_name, + lines=Lines( + begin=lines[0] + ) + ) + ) + self.print_trace(f"Created snippet CodeQuality object: {code_quality}") + return code_quality + + def _write_output(self, data: list[CodeQuality], output_file: str = None) -> bool: + """Write the Gitlab Code Quality Report to output.""" + self.print_trace(f"_write_output called with {len(data)} items, output_file: {output_file}") + try: + json_data = [item.to_dict() for item in data] + self.print_trace(f"JSON data: {json_data}") + file = open(output_file, 'w') if output_file else sys.stdout + print(json.dumps(json_data, indent=2), file=file) + if output_file: + file.close() + self.print_debug(f"Wrote output to file: {output_file}") + else: + self.print_debug("Wrote output to 'stdout'") + return True + except Exception as e: + self.print_stderr(f'Error writing output: {str(e)}') + return False + + def _produce_from_json(self, data: dict, output_file: str = None) -> bool: + self.print_trace(f"_produce_from_json called with output_file: {output_file}") + self.print_debug(f"Processing {len(data)} files from JSON data") + code_quality = [] + for file_name, results in data.items(): + self.print_trace(f"Processing file: {file_name} with {len(results)} results") + for result in results: + if not result.get('id'): + self.print_debug(f"Warning: No ID found for result: {result}") + continue + if result.get('id') != 'snippet' and result.get('id') != 'file': + self.print_debug(f"Skipping non-snippet/file match: {file_name}, id: '{result['id']}'") + continue + code_quality_item = self._get_code_quality(file_name, result) + if code_quality_item: + code_quality.append(code_quality_item) + self.print_trace(f"Added code quality item for {file_name}") + else: + self.print_debug(f"Warning: No Code Quality found for result: {result}") + self.print_debug(f"Generated {len(code_quality)} code quality items") + self._write_output(data=code_quality,output_file=output_file) + return True + + def _produce_from_str(self, json_str: str, output_file: str = None) -> bool: + """ + Produce Gitlab Code Quality Report output from input JSON string + :param json_str: input JSON string + :param output_file: Output file (optional) + :return: True if successful, False otherwise + """ + self.print_trace(f"_produce_from_str called with output_file: {output_file}") + if not json_str: + self.print_stderr('ERROR: No JSON string provided to parse.') + return False + self.print_debug(f"Parsing JSON string of length: {len(json_str)}") + try: + data = json.loads(json_str) + self.print_debug("Successfully parsed JSON data") + self.print_trace(f"Parsed data structure: {type(data)}") + except Exception as e: + self.print_stderr(f'ERROR: Problem parsing input JSON: {e}') + return False + return self._produce_from_json(data, output_file) + + + def produce_from_file(self, json_file: str, output_file: str = None) -> bool: + """ + Parse plain/raw input JSON file and produce GitLab Code Quality JSON output + :param json_file: + :param output_file: + :return: True if successful, False otherwise + """ + self.print_trace(f"produce_from_file called with json_file: {json_file}, output_file: {output_file}") + self.print_debug(f"Input JSON file: {json_file}, output_file: {output_file}") + if not json_file: + self.print_stderr('ERROR: No JSON file provided to parse.') + return False + if not os.path.isfile(json_file): + self.print_stderr(f'ERROR: JSON file does not exist or is not a file: {json_file}') + return False + self.print_debug(f"Reading JSON file: {json_file}") + with open(json_file, 'r') as f: + json_content = f.read() + success = self._produce_from_str(json_content, output_file) + return success diff --git a/src/scanoss/header_filter.py b/src/scanoss/header_filter.py new file mode 100644 index 00000000..0b6869dd --- /dev/null +++ b/src/scanoss/header_filter.py @@ -0,0 +1,560 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. + + Line Filter Module - Identifies where real source code implementation begins. + + This module analyzes source code files and determines which lines are: + - License headers + - Documentation comments + - Imports/includes + - Blank lines + + And returns the content from where the real implementation begins. +""" + +import re +from pathlib import Path +from typing import Optional, Tuple + +from .scanossbase import ScanossBase + + +class LanguagePatterns: + """ + Regex patterns for different programming languages. + + This class provides a collection of regex patterns for identifying different + programming constructs, handling imports, comments, and license statements + across various programming languages. The main purpose of this class is to + assist in parsing or analysing code written in different languages efficiently. + + :ivar COMMENT_PATTERNS: A dictionary containing regex patterns to identify + single-line and multi-line comments in various programming languages. + :ivar IMPORT_PATTERNS: A dictionary mapping programming languages to their + respective regex patterns for identifying import statements or package + includes it. + :ivar LICENSE_KEYWORDS: A list of keywords commonly found in license texts + or statements, often used to detect the presence of licensing information. + """ + # Comment patterns (single-line and multi-line start/end) + COMMENT_PATTERNS = { + # C-style languages: C, C++, Java, JavaScript, TypeScript, Go, + # Rust, C#, PHP, Kotlin, Scala, Dart, Objective-C + 'c_style': { + 'single_line': r'^\s*//.*$', + 'multi_start': r'^\s*/\*', + 'multi_end': r'\*/\s*$', + 'multi_single': r'^\s*/\*.*\*/\s*$', + }, + # Python, shell scripts, Ruby, Perl, R, Julia, YAML + 'python_style': { + 'single_line': r'^\s*#.*$', + 'doc_string_start': r'^\s*"""', + 'doc_string_end': r'"""\s*$', + }, + # Lua, SQL, Haskell + 'lua_style': { + 'single_line': r'^\s*--.*$', + 'multi_start': r'^\s*--\[\[', + 'multi_end': r'\]\]\s*$', + }, + # HTML, XML + 'html_style': { + 'multi_start': r'^\s*\s*$', + 'multi_single': r'^\s*\s*$', + }, + } + # Import/include patterns by language + IMPORT_PATTERNS = { + 'python': [ + r'^\s*import\s+', + r'^\s*from\s+.*\s+import\s+', + ], + 'javascript': [ + r'^\s*import\s+.*\s+from\s+', + r'^\s*import\s+["\']', + r'^\s*import\s+type\s+', + r'^\s*export\s+\*\s+from\s+', + r'^\s*export\s+\{.*\}\s+from\s+', + r'^\s*const\s+.*\s*=\s*require\(', + r'^\s*var\s+.*\s*=\s*require\(', + r'^\s*let\s+.*\s*=\s*require\(', + ], + 'typescript': [ + r'^\s*import\s+', + r'^\s*export\s+.*\s+from\s+', + r'^\s*import\s+type\s+', + r'^\s*import\s+\{.*\}\s+from\s+', + ], + 'java': [ + r'^\s*import\s+', + r'^\s*package\s+', + ], + 'kotlin': [ + r'^\s*import\s+', + r'^\s*package\s+', + ], + 'scala': [ + r'^\s*import\s+', + r'^\s*package\s+', + ], + 'go': [ + r'^\s*import\s+\(', + r'^\s*import\s+"', + r'^\s*package\s+', + r'^\s*"[^"]*"\s*$', # Imports inside import () block + # Imports with alias: name "package" + r'^\s*[a-zA-Z_][a-zA-Z0-9_]*\s+"[^"]*"\s*$', + r'^\s*_\s+"[^"]*"\s*$', # _ "package" imports + ], + 'rust': [ + r'^\s*use\s+', + r'^\s*extern\s+crate\s+', + r'^\s*mod\s+', + ], + 'cpp': [ + r'^\s*#include\s+', + r'^\s*#pragma\s+', + r'^\s*#ifndef\s+.*_H.*', # Header guards: #ifndef FOO_H + r'^\s*#define\s+.*_H.*', # Header guards: #define FOO_H + # #endif at end of file (may have comment) + r'^\s*#endif\s+(//.*)?\s*$', + ], + 'csharp': [ + r'^\s*using\s+', + r'^\s*namespace\s+', + ], + 'php': [ + r'^\s*use\s+', + r'^\s*require\s+', + r'^\s*require_once\s+', + r'^\s*include\s+', + r'^\s*include_once\s+', + r'^\s*namespace\s+', + ], + 'swift': [ + r'^\s*import\s+', + ], + 'ruby': [ + r'^\s*require\s+', + r'^\s*require_relative\s+', + r'^\s*load\s+', + ], + 'perl': [ + r'^\s*use\s+', + r'^\s*require\s+', + ], + 'r': [ + r'^\s*library\(', + r'^\s*require\(', + r'^\s*source\(', + ], + 'lua': [ + r'^\s*require\s+', + r'^\s*local\s+.*\s*=\s*require\(', + ], + 'dart': [ + r'^\s*import\s+', + r'^\s*export\s+', + r'^\s*part\s+', + ], + 'haskell': [ + r'^\s*import\s+', + r'^\s*module\s+', + ], + 'elixir': [ + r'^\s*import\s+', + r'^\s*alias\s+', + r'^\s*require\s+', + r'^\s*use\s+', + ], + 'clojure': [ + r'^\s*\(\s*ns\s+', + r'^\s*\(\s*require\s+', + r'^\s*\(\s*import\s+', + ], + } + # Keywords that indicate licenses + LICENSE_KEYWORDS = [ + 'copyright', 'license', 'licensed', 'all rights reserved', + 'permission', 'redistribution', 'warranty', 'liability', + 'apache', 'mit', 'gpl', 'bsd', 'mozilla', 'author:', + 'spdx-license', 'contributors', 'licensee' + ] + +COMPLETE_DOCSTRING_QUOTE_COUNT = 2 +LICENSE_HEADER_MAX_LINES = 50 +# Map of file extensions to programming languages +EXT_MAP = { + '.py': 'python', + '.js': 'javascript', + '.mjs': 'javascript', + '.cjs': 'javascript', + '.ts': 'typescript', + '.tsx': 'typescript', + '.jsx': 'javascript', + '.java': 'java', + '.kt': 'kotlin', + '.kts': 'kotlin', + '.scala': 'scala', + '.sc': 'scala', + '.go': 'go', + '.rs': 'rust', + '.cpp': 'cpp', + '.cc': 'cpp', + '.cxx': 'cpp', + '.c': 'cpp', + '.h': 'cpp', + '.hpp': 'cpp', + '.hxx': 'cpp', + '.cs': 'csharp', + '.php': 'php', + '.swift': 'swift', + '.rb': 'ruby', + '.pl': 'perl', + '.pm': 'perl', + '.r': 'r', + '.R': 'r', + '.lua': 'lua', + '.dart': 'dart', + '.hs': 'haskell', + '.ex': 'elixir', + '.exs': 'elixir', + '.clj': 'clojure', + '.cljs': 'clojure', + '.m': 'cpp', # Objective-C + '.mm': 'cpp', # Objective-C++ + # Shell scripts share Python's # comment style, but lack dedicated + # import patterns (source/. commands won't be filtered) + '.sh': 'python', + '.bash': 'python', + '.zsh': 'python', + '.fish': 'python', +} + + +def is_blank_line(stripped_line: str) -> bool: + """ + Check if a line is blank. + + This method determines whether a given string `line` is blank by checking + if it consists entirely of whitespace or is empty. + + :param stripped_line: The string to be evaluated. + :return: True if the string is blank, otherwise False. + """ + return len(stripped_line) == 0 + + +def is_shebang(stripped_line: str) -> bool: + """ + Check if the given line is a shebang line. + + This function determines if the provided string is a shebang line, + which indicates the path to the interpreter that should execute the + script. + + :param stripped_line: The string to check if it's a shebang line. + :return: True if the given line starts with '#!', otherwise False. + """ + return stripped_line.startswith('#!') + + +class HeaderFilter(ScanossBase): + """ + Source code file analyser that filters headers, comments, and imports. + + This class processes code files and returns only the real + implementation content, omitting licenses, documentation comments, + and imports. + """ + + def __init__( + self, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + skip_limit: Optional[int] = None + ): + """ + Initialise HeaderFilter + Parameters + ---------- + skip_limit: int + Maximum number of lines to skip when analysing a file. + If set, then stop stripping data after this number of lines. + (None/0 = unlimited by default) + """ + super().__init__(debug, trace, quiet) + self.patterns = LanguagePatterns() + self.max_lines = skip_limit + + def filter(self, file: str, decoded_contents: str) -> int: + """ + Main method that filters file content + Parameters + ---------- + :param file: File path (used to detect extension) + :param decoded_contents: File contents in utf-8 encoding + Return + ------ + - line_offset: Number of lines skipped from the beginning + (0 if no filtering) + """ + if not decoded_contents or not file: + self.print_msg(f'No file or contents provided, skipping line filter for: {file}') + return 0 + self.print_debug(f'HeaderFilter processing file: {file}') + # Detect language + language = self.detect_language(file) + # If language is not supported, return original content + if not language: + self.print_debug(f'Skipping line filter for unsupported language: {file}') + return 0 + lines = decoded_contents.splitlines(keepends=True) + num_lines = len(lines) + if num_lines == 0: + self.print_msg(f'No lines in file: {file}') + return 0 + self.print_debug(f'Analysing {num_lines} lines for file: {file}') + + # Find the first implementation line (optimised - stops at first match) + implementation_start = self.find_first_implementation_line(lines, language) + # If no implementation, return empty + if implementation_start is None: + self.print_debug(f'No implementation found in file: {file}') + return 0 + # Calculate how many lines were filtered out (line_offset) + line_offset = implementation_start - 1 + # Apply max_lines limit if configured + if self.max_lines is not None and 0 < self.max_lines < line_offset: + self.print_trace( + f'Line offset {line_offset} exceeds max_lines {self.max_lines}, ' + f'capping at {self.max_lines} for: {file}' + ) + line_offset = self.max_lines + + if line_offset > 0: + self.print_debug(f'Filtered out {line_offset} lines from beginning of {file} (language: {language})') + return line_offset + + def detect_language(self, file_path: str) -> Optional[str]: + """ + Detects the programming language based on the provided file extension. + + This function uses a predefined mapping between file extensions and programming + languages to determine the language associated with the file. If the file extension + is found in the mapping, the corresponding language is returned. Otherwise, it + returns None. + + :param file_path: Path to the file whose programming language needs to be detected. + :return: The programming language corresponding to the file extension if mapped, + otherwise None. + """ + path = Path(file_path) + extension = path.suffix.lower() + if extension: + detected_language = EXT_MAP.get(extension) + if detected_language: + self.print_debug(f'Detected language "{detected_language}" for extension "{extension}"') + else: + self.print_debug(f'No language mapping found for extension "{extension}"') + else: + self.print_debug(f'No file extension found, skipping language detection for: {file_path}') + detected_language = None + return detected_language + + def is_license_header(self, line: str) -> bool: + """ + Check if the line appears to be part of a license header. + + This method evaluates a given line of text to determine whether it + contains keywords that suggest it is part of a license header. It + performs a case-insensitive check against a predefined set of license + keywords. + + :param line: The line of text to check. + :return: True if the line contains keywords indicating it is part of a + license header; False otherwise. + """ + line_lower = line.lower() + return any(keyword in line_lower for keyword in self.patterns.LICENSE_KEYWORDS) + + def get_comment_style(self, language: str) -> str: + """ + Return the comment style associated with a given programming language. + + This method determines the appropriate comment style to use based on the + specified programming language. Supported languages include those with C-style + comments, Python-style comments, and Lua-style comments. If the language does + not match any of the explicitly defined groups, a default of `c_style` is + returned. + + :param language: The name of the programming language for which the comment + style needs to be determined. + :return: The comment style for the provided programming language. Possible + values are 'c_style', 'python_style', or 'lua_style'. + """ + if language: + if language in ['cpp', 'java', 'kotlin', 'scala', 'javascript', 'typescript', + 'go', 'rust', 'csharp', 'php', 'swift', 'dart']: + return 'c_style' + if language in ['python', 'ruby', 'perl', 'r']: + return 'python_style' + if language in ['lua', 'haskell']: + return 'lua_style' + self.print_debug(f'No comment style defined for language "{language}", using default: "c_style"') + return 'c_style' # Default + + def is_comment(self, line: str, in_multiline: bool, patterns: dict) -> Tuple[bool, bool]: # noqa: PLR0911 + """ + Check if a line is a comment + + :param patterns: comment patterns + :param line: Line to check + :param in_multiline: Whether we're currently in a multiline comment + :return: Tuple of (is_comment, still_in_multiline) + """ + if not patterns: + self.print_msg('No comment patterns defined, skipping comment check') + return False, in_multiline + # If we're in a multiline comment + if in_multiline: + # Check if the comment ends + if 'multi_end' in patterns and re.search(patterns['multi_end'], line): + return True, False + if 'doc_string_end' in patterns and re.search(patterns['doc_string_end'], line): + return True, False + return True, True + # Single-line comment + if 'single_line' in patterns and re.match(patterns['single_line'], line): + return True, False + # Multiline comment complete in one line + if 'multi_single' in patterns and re.match(patterns['multi_single'], line): + return True, False + # Start of multiline comment (C-style) + if 'multi_start' in patterns and re.search(patterns['multi_start'], line): + # If it also ends on the same line + if 'multi_end' in patterns and re.search(patterns['multi_end'], line): + return True, False + return True, True + # Start of docstring (Python) + if 'doc_string_start' in patterns and '"""' in line: + # Count how many quotes there are + count = line.count('"""') + if count == COMPLETE_DOCSTRING_QUOTE_COUNT: # Complete docstring in one line + return True, False + if count == 1: # Start of a multiline docstring + return True, True + # Default response: not a comment + return False, in_multiline + + def is_import(self, line: str, patterns: dict) -> bool: + """ + Check if a line of code is an import or include statement for a given programming language. + + This function determines whether a specific line of code matches any + import/include patterns defined for the provided programming language. + It relies on predefined regular expression patterns. + + :param patterns: import patterns for the given language. + :param line: A single line of code to check. + :return: True if the line matches any import/include pattern for the given language, + otherwise False. + """ + if not patterns: + self.print_debug('No import patterns defined, skipping import check') + return any(re.match(pattern, line) for pattern in patterns) + + def find_first_implementation_line(self, lines: list[str], language: str) -> Optional[int]: # noqa: PLR0912 + """ + Find the line number where the implementation begins (optimised version). + Returns as soon as the first implementation line is found. + + :param lines: List of code lines + :param language: Programming language + :return: Line number (1-indexed) where implementation starts, or None if not found + """ + if not lines or not language: + self.print_debug('No lines or language provided, skipping implementation line detection') + return None + in_multiline_comment = False + in_license_section = False + inside_multiline_import = False # To handle multi-line import blocks + found_imports = False + # Get comment & import patterns for the language + comment_patterns = self.patterns.COMMENT_PATTERNS[self.get_comment_style(language)] + import_patterns = self.patterns.IMPORT_PATTERNS[language] + # Iterate through lines trying to find the first implementation line + for i, line in enumerate(lines): + line_number = i + 1 + stripped = line.strip() + # Shebang (only first line) or blank line + if (i == 0 and is_shebang(stripped)) or is_blank_line(stripped): + continue + # Check if it's a comment + is_a_comment, in_multiline_comment = self.is_comment(line, in_multiline_comment, comment_patterns) + if is_a_comment: + # Check if it's part of the license header + if self.is_license_header(line): + if not in_license_section: + self.print_trace(f'Line {line_number}: Detected license header section') + in_license_section = True + # If still in the license section (first lines) + elif in_license_section and line_number < LICENSE_HEADER_MAX_LINES: + pass # Still in the license section. Keep looking. + else: + if in_license_section: + self.print_trace(f'Line {line_number}: End of license header section') + in_license_section = False + continue + # If not a comment but we find a non-empty line, end license section + if not is_a_comment: + in_license_section = False + # Check if this line closes a multi-line import block + if inside_multiline_import and (')' in stripped or '}' in stripped): + self.print_trace(f'Line {line_number}: Multi-line import block end') + inside_multiline_import = False + continue + # Skip continuation lines inside multi-line import blocks (e.g. "DEFAULT_SYFT_COMMAND,") + if inside_multiline_import: + continue + # Check if it's an import + if self.is_import(line, import_patterns): + if not found_imports: + self.print_trace(f'Line {line_number}: Detected import section') + found_imports = True + # Detect start of multi-line import block (e.g. "from x import (", "import {") + if ('(' in stripped and ')' not in stripped) or ('{' in stripped and '}' not in stripped): + self.print_trace(f'Line {line_number}: Multi-line import block start') + inside_multiline_import = True + continue + # If we get here, it's implementation code - return immediately! + self.print_trace(f'Line {line_number}: First implementation line detected') + return line_number + # End for loop? + return None +# +# End of HeaderFilter Class +# \ No newline at end of file diff --git a/src/scanoss/inspection/__init__.py b/src/scanoss/inspection/__init__.py new file mode 100644 index 00000000..1e95c46d --- /dev/null +++ b/src/scanoss/inspection/__init__.py @@ -0,0 +1,23 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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/src/scanoss/inspection/policy_check/__init__.py b/src/scanoss/inspection/policy_check/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/scanoss/inspection/policy_check/dependency_track/__init__.py b/src/scanoss/inspection/policy_check/dependency_track/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/scanoss/inspection/policy_check/dependency_track/project_violation.py b/src/scanoss/inspection/policy_check/dependency_track/project_violation.py new file mode 100644 index 00000000..e9d78667 --- /dev/null +++ b/src/scanoss/inspection/policy_check/dependency_track/project_violation.py @@ -0,0 +1,479 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" +import json +import time +from datetime import datetime +from typing import Any, Dict, List, Optional, TypedDict + +from ....services.dependency_track_service import DependencyTrackService +from ...utils.markdown_utils import generate_jira_table, generate_table +from ..policy_check import PolicyCheck, PolicyOutput, PolicyStatus + +# Constants +PROCESSING_RETRY_DELAY = 5 # seconds +DEFAULT_TIME_OUT = 300.0 +MILLISECONDS_TO_SECONDS = 1000 + +""" +Dependency Track project violation policy check implementation. + +This module provides policy checking functionality for Dependency Track project violations. +It retrieves, processes, and formats policy violations from a Dependency Track instance +for a specific project. +""" + + +class ResolvedLicenseDict(TypedDict): + """TypedDict for resolved license information from Dependency Track.""" + uuid: str + name: str + licenseId: str + isOsiApproved: bool + isFsfLibre: bool + isDeprecatedLicenseId: bool + isCustomLicense: bool + + +class ProjectDict(TypedDict): + """TypedDict for project information from Dependency Track.""" + authors: List[str] + name: str + version: str + classifier: str + collectionLogic: str + uuid: str + properties: List[Any] + tags: List[str] + lastBomImport: int + lastBomImportFormat: str + lastInheritedRiskScore: float + lastVulnerabilityAnalysis: int + active: bool + isLatest: bool + + +class ComponentDict(TypedDict): + """TypedDict for component information from Dependency Track.""" + authors: List[str] + name: str + version: str + classifier: str + purl: str + purlCoordinates: str + resolvedLicense: ResolvedLicenseDict + project: ProjectDict + lastInheritedRiskScore: float + uuid: str + expandDependencyGraph: bool + isInternal: bool + cpe: Optional[str] + + +class PolicyDict(TypedDict): + """TypedDict for policy information from Dependency Track.""" + name: str + operator: str + violationState: str + uuid: str + includeChildren: bool + onlyLatestProjectVersion: bool + + +class PolicyConditionDict(TypedDict): + """TypedDict for policy condition information from Dependency Track.""" + policy: PolicyDict + operator: str + subject: str + value: str + uuid: str + + +class PolicyViolationDict(TypedDict): + """TypedDict for policy violation information from Dependency Track.""" + type: str + project: ProjectDict + component: ComponentDict + policyCondition: PolicyConditionDict + timestamp: int + uuid: str + + +class DependencyTrackProjectViolationPolicyCheck(PolicyCheck[PolicyViolationDict]): + """ + Policy check implementation for Dependency Track project violations. + + This class handles retrieving, processing, and formatting policy violations + from a Dependency Track instance for a specific project. + """ + + def __init__( # noqa: PLR0913 + self, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + project_id: str = None, + project_name: str = None, + project_version: str = None, + api_key: str = None, + url: str = None, + upload_token: str = None, + timeout: float = DEFAULT_TIME_OUT, + format_type: str = None, + status: str = None, + output: str = None, + ): + """ + Initialise the Dependency Track project violation policy checker. + + Args: + debug: Enable debug output + trace: Enable trace output + quiet: Enable quiet mode + project_id: UUID of the project in Dependency Track + project_name: Name of the project in Dependency Track + project_version: Version of the project in Dependency Track + api_key: API key for Dependency Track authentication + url: Base URL of the Dependency Track instance + upload_token: Upload token for uploading BOMs to Dependency Track + format_type: Output format type (json, markdown, etc.) + status: Status output destination + output: Results output destination + timeout: Timeout for processing in seconds (default: 300) + """ + super().__init__(debug, trace, quiet, format_type, status, 'dependency-track', output) + self.api_key = api_key + self.project_id = project_id + self.project_name = project_name + self.project_version = project_version + self.upload_token = upload_token + self.timeout = timeout + self.url = url.strip().rstrip('/') if url else None + self.dep_track_service = DependencyTrackService(self.api_key, self.url, debug=debug, trace=trace, quiet=quiet) + + def _json(self, project_violations: list[PolicyViolationDict]) -> PolicyOutput: + """ + Format project violations as JSON. + + Args: + project_violations: List of policy violations from Dependency Track + + Returns: + Dictionary containing JSON formatted results and summary + """ + return PolicyOutput( + details= json.dumps(project_violations, indent=2), + summary= f'{len(project_violations)} policy violations were found.\n', + ) + + def _markdown(self, project_violations: list[PolicyViolationDict]) -> PolicyOutput: + """ + Format Dependency Track violations to Markdown format. + + Args: + project_violations: List of policy violations from Dependency Track + + Returns: + Dictionary with formatted Markdown details and summary + """ + return self._md_summary_generator(project_violations, generate_table) + + def _jira_markdown(self, data: list[PolicyViolationDict]) -> PolicyOutput: + """ + Format project violations for Jira Markdown. + + Args: + data: List of policy violations from Dependency Track + + Returns: + Dictionary containing Jira markdown formatted results and summary + """ + return self._md_summary_generator(data, generate_jira_table) + + def is_project_updated(self, dt_project: Dict[str, Any]) -> bool: + """ + Check if a Dependency Track project has completed processing. + + This method determines if a project has finished processing by comparing + the timestamps of the last BOM import, vulnerability analysis, and last + occurrence metrics. A project is considered updated when either the + vulnerability analysis or the metrics' last occurrence timestamp is greater + than or equal to the last BOM import timestamp. + + Args: + dt_project: Project dictionary from Dependency Track containing + project metadata and timestamps + + Returns: + True if the project has completed processing (vulnerability analysis + or metrics are up to date with the last BOM import), False otherwise + """ + if not dt_project: + self.print_stderr('Warning: No project details supplied. Returning False.') + return False + + # Safely extract and normalise timestamp values to numeric types + def _safe_timestamp(field, value=None, default=0) -> float: + """Convert timestamp value to float, handling string/numeric types safely.""" + if value is None: + return float(default) + try: + return float(value) + except (ValueError, TypeError): + self.print_stderr(f'Warning: Invalid timestamp for {field}, value: {value}, using default: {default}') + return float(default) + + last_import = _safe_timestamp('lastBomImport', dt_project.get('lastBomImport'), 0) + last_vulnerability_analysis = _safe_timestamp('lastVulnerabilityAnalysis', + dt_project.get('lastVulnerabilityAnalysis'), 0 + ) + metrics = dt_project.get('metrics', {}) + last_occurrence = _safe_timestamp('lastOccurrence', + metrics.get('lastOccurrence', 0) + if isinstance(metrics, dict) else 0, 0 + ) + if self.debug: + self.print_msg(f'last_import: {last_import}') + self.print_msg(f'last_vulnerability_analysis: {last_vulnerability_analysis}') + self.print_msg(f'last_occurrence: {last_occurrence}') + self.print_msg(f'last_vulnerability_analysis is updated: {last_vulnerability_analysis >= last_import}') + self.print_msg(f'last_occurrence is updated: {last_occurrence >= last_import}') + # Catches case where vulnerability analysis is skipped for empty SBOMs + if 0 < last_import <= last_occurrence: + component_count = metrics.get('components', 0) if isinstance(metrics, dict) else 0 + if component_count < 1: + self.print_msg('Notice: Empty SBOM detected. Assuming no violations.') + return True + # If all timestamps are zero, this indicates no processing has occurred + if last_vulnerability_analysis == 0 or last_occurrence == 0 or last_import == 0: + self.print_stderr(f'Warning: Some project data appears to be unset. Returning False: {dt_project}') + return False + # True if: Both vulnerability analysis and metrics calculation newer than last BOM upload + return last_vulnerability_analysis >= last_import and last_occurrence >= last_import + + def _wait_processing_by_project_id(self) -> Optional[Any] or None: + """ + Wait for project processing to complete in Dependency Track. + + Returns: + Return the project or None if processing fails or times out + """ + start_time = time.time() + while True: + self.print_debug('Starting...') + dt_project = self.dep_track_service.get_project_by_id(self.project_id) + if not dt_project: + self.print_stderr(f'Failed to get project by id: {self.project_id}') + return None + is_project_updated = self.is_project_updated(dt_project) + if is_project_updated: # Project updated, return it + return dt_project + # Check timeout + if time.time() - start_time > self.timeout: + self.print_msg(f'Warning: Timeout reached ({self.timeout}s) while waiting for project processing') + return dt_project + time.sleep(PROCESSING_RETRY_DELAY) + self.print_debug('Checking if complete...') + # End while loop + + def _wait_processing_by_project_status(self): + """ + Wait for project processing to complete in Dependency Track. + + Returns: + Project status dictionary or None if processing fails or times out + """ + start_time = time.time() + while True: + status = self.dep_track_service.get_project_status(self.upload_token) + if status is None: + self.print_stderr(f'Error getting project status for upload token: {self.upload_token}') + break + if status and not status.get('processing'): + self.print_debug(f'Project Status: {status}') + break + if time.time() - start_time > self.timeout: + self.print_msg(f'Timeout reached ({self.timeout}s) while waiting for project processing') + break + time.sleep(PROCESSING_RETRY_DELAY) + self.print_debug('Checking if complete...') + # End while loop + + def _wait_project_processing(self): + """ + Wait for project processing to complete in Dependency Track. + + Returns: + Project status dictionary or None if processing fails + """ + if self.upload_token: + self.print_debug("Using upload token to check project status") + self._wait_processing_by_project_status() + self.print_debug("Using project id to get project status") + return self._wait_processing_by_project_id() + + def _set_project_id(self) -> None: + """ + Set the project ID based on the project name and version if not already set. + If no project id is specified, this method will attempt to retrieve the project based on name/version. + + Raises: + ValueError: If the project name/version is missing or the project is not found. + RuntimeError: If there's an error communicating with Dependency Track. + """ + if self.project_id is not None: + return + if self.project_name is None or self.project_version is None: + raise ValueError( + "Error: Project name and version must be specified when not using project ID" + ) + self.print_debug(f'Searching for project id by name and version: {self.project_name}@{self.project_version}') + dt_project = self.dep_track_service.get_project_by_name_version(self.project_name, self.project_version) + self.print_debug(f'dt_project: {dt_project}') + if dt_project is None: + raise ValueError(f'Error: Project {self.project_name}@{self.project_version} not found in Dependency Track') + self.project_id = dt_project.get('uuid') + if not self.project_id: + self.print_stderr(f'Error: Failed to get project uuid from: {dt_project}') + raise ValueError(f'Error: Project {self.project_name}@{self.project_version} does not have a valid UUID') + + def _sort_project_violations(self,violations: List[PolicyViolationDict]) -> List[PolicyViolationDict]: + """ + Sort project violations by priority. + + Sorts violations with SECURITY issues first, followed by LICENSE, + then OTHER types. + + Args: + violations: List of policy violation dictionaries + + Returns: + Sorted list of policy violations + """ + type_priority = {'SECURITY': 3, 'LICENSE': 2, 'OTHER': 1} + return sorted( + violations, + key=lambda x: -type_priority.get(x.get('type', 'OTHER'), 1) + ) + + def _md_summary_generator(self, project_violations: list[PolicyViolationDict], table_generator) -> PolicyOutput: + """ + Generates a Markdown summary of project policy violations. + + Args: + project_violations (list[PolicyViolationDict]): A list of dictionaries containing details of + project policy violations, including violation state, risk type, policy name, component details, + and timestamp. + table_generator (function): A callable function responsible for generating the Markdown table + using headers, rows, and optionally highlighted columns. + + Returns: + dict: A dictionary with two keys: + - "details" containing a Markdown-compatible string with detailed project violations + rendered as a table + - "summary" summarising the number of violations found + """ + if project_violations is None: + self.print_stderr('Warning: No project violations found. Returning empty results.') + return PolicyOutput( + details= "h3. Dependency Track Project Violations\n\nNo policy violations found.\n", + summary= "0 policy violations were found.\n", + ) + headers = ['State', 'Risk Type', 'Policy Name', 'Component', 'Date'] + c_cols = [0, 1] + rows: List[List[str]] = [] + + for project_violation in project_violations: + timestamp = project_violation['timestamp'] + timestamp_seconds = timestamp / MILLISECONDS_TO_SECONDS + formatted_date = datetime.fromtimestamp(timestamp_seconds).strftime("%d %b %Y at %H:%M:%S") + + purl = project_violation["component"]["purl"] + version = project_violation["component"]["version"] + # If PURL doesn't contain version but version is available, append it + component_display = purl + if version and '@' not in purl: + component_display = f'{purl}@{version}' + row = [ + project_violation['policyCondition']["policy"]["violationState"], + project_violation['type'], + project_violation['policyCondition']["policy"]["name"], + component_display, + formatted_date, + ] + rows.append(row) + # End for loop + return PolicyOutput( + details= f'### Dependency Track Project Violations\n{table_generator(headers, rows, c_cols)}\n\n' + f'View project in Dependency Track [here]({self.url}/projects/{self.project_id}).\n', + summary= f'{len(project_violations)} policy violations were found.\n' + ) + + def run(self) -> int: + """ + Runs the primary execution logic of the instance. + + Returns: + int: Status code indicating the result of the run process. Possible + values are derived from the PolicyStatus enumeration. + FAIL if violations are found, SUCCESS if no violations are found, ERROR if an error occurs. + + Raises: + ValueError: If an invalid format is specified during the execution. + """ + # Set project ID based on name/version if needed + self._set_project_id() + if self.debug: + self.print_msg(f'URL: {self.url}') + self.print_msg(f'Project Id: {self.project_id}') + self.print_msg(f'Project Name: {self.project_name}') + self.print_msg(f'Project Version: {self.project_version}') + self.print_msg(f'API Key: {"*" * len(self.api_key)}') + self.print_msg(f'Format: {self.format_type}') + self.print_msg(f'Status: {self.status}') + self.print_msg(f'Output: {self.output}') + self.print_msg(f'Timeout: {self.timeout}') + # Confirm processing is complete before returning project violations + dt_project = self._wait_project_processing() + if not dt_project: + return PolicyStatus.ERROR.value + # Get project violations from Dependency Track + dt_project_violations = self.dep_track_service.get_project_violations(self.project_id) + # Handle case where service returns None (API error) vs empty list (no violations) + if dt_project_violations is None: + self.print_stderr('Error: Failed to retrieve project violations from Dependency Track') + return PolicyStatus.ERROR.value + # Sort violations by priority and format output + formatter = self._get_formatter() + if formatter is None: + self.print_stderr('Error: Invalid format specified.') + return PolicyStatus.ERROR.value + # Format and output data - handle empty results gracefully + policy_output = formatter(self._sort_project_violations(dt_project_violations)) + self.print_to_file_or_stdout(policy_output.details, self.output) + self.print_to_file_or_stderr(policy_output.summary, self.status) + # Return appropriate status based on violation count + if len(dt_project_violations) > 0: + return PolicyStatus.POLICY_FAIL.value + return PolicyStatus.POLICY_SUCCESS.value + diff --git a/src/scanoss/inspection/policy_check/policy_check.py b/src/scanoss/inspection/policy_check/policy_check.py new file mode 100644 index 00000000..89263a30 --- /dev/null +++ b/src/scanoss/inspection/policy_check/policy_check.py @@ -0,0 +1,222 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +from abc import ABC, abstractmethod +from enum import Enum +from typing import Callable, Dict, Generic, List, NamedTuple, TypeVar + +from ...scanossbase import ScanossBase +from ..utils.license_utils import LicenseUtil + + +class PolicyStatus(Enum): + """ + Enumeration representing the status of a policy check. + + Attributes: + POLICY_SUCCESS (int): Indicates that the policy check passed successfully (value: 0). + POLICY_FAIL (int): Indicates that the policy check failed (value: 2). + ERROR (int): Indicates that an error occurred during the policy check (value: 1). + """ + POLICY_SUCCESS = 0 + POLICY_FAIL = 2 + ERROR = 1 +# +# End of PolicyStatus Class +# + +class PolicyOutput(NamedTuple): + details: str + summary: str + +T = TypeVar('T') + +class PolicyCheck(ScanossBase, Generic[T], ABC): + """ + A base class for implementing various software policy checks. + + This class provides a framework for policy checking, including methods for + processing components, generating output in different formats. + + Attributes: + VALID_FORMATS (set): A set of valid output formats ('md', 'json'). + + Inherits from: + InspectBase: A base class providing common functionality for SCANOSS-related operations. + """ + VALID_FORMATS = {'md', 'json', 'jira_md'} + def __init__( # noqa: PLR0913 + self, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + format_type: str = None, + status: str = None, + name: str = None, + output: str = None, + ): + super().__init__(debug, trace, quiet) + self.license_util = LicenseUtil() + self.name = name + self.format_type = format_type + self.status = status + self.output = output + + @abstractmethod + def run(self)-> tuple[int,PolicyOutput]: + """ + Execute the policy check process. + + This abstract method should be implemented by subclasses to perform specific + policy checks. The general structure of this method typically includes: + 1. Retrieving components + 2. Filtering components based on specific criteria + 3. Formatting the results + 4. Saving the output to files if required + + :return: A named tuple containing two elements: + - First element: PolicyStatus enum value (SUCCESS, FAIL, or ERROR) + - Second element: PolicyOutput A tuple containing the policy results. + """ + pass + + @abstractmethod + def _json(self, data: list[T]) -> PolicyOutput: + """ + Format the policy checks results as JSON. + This method should be implemented by subclasses to create a Markdown representation + of the policy check results. + + :param data: List of data to be formatted. + :return: A dictionary containing two keys: + - 'results': A JSON-formatted string with the full list of components + - 'summary': A string summarizing the number of components found + """ + pass + + @abstractmethod + def _markdown(self, data: list[T]) -> PolicyOutput: + """ + Generate Markdown output for the policy check results. + + This method should be implemented by subclasses to create a Markdown representation + of the policy check results. + + :param data: List of data to be included in the output. + :return: A dictionary representing the Markdown output. + """ + pass + + @abstractmethod + def _jira_markdown(self, data: list[T]) -> PolicyOutput: + """ + Generate Markdown output for the policy check results. + + This method should be implemented by subclasses to create a Markdown representation + of the policy check results. + + :param data: List of data to be included in the output. + :return: A dictionary representing the Markdown output. + """ + pass + + def _get_formatter(self) -> Callable[[List[dict]], PolicyOutput]: + """ + Get the appropriate formatter function based on the specified format. + + :return: Formatter function (either _json or _markdown) + """ + valid_format = self._is_valid_format() + if not valid_format: + raise ValueError('Invalid format specified') + # a map of which format function to return + function_map = { + 'json': self._json, + 'md': self._markdown, + 'jira_md': self._jira_markdown, + } + return function_map[self.format_type] + + def _debug(self): + """ + Print debug information about the policy check. + + This method prints various attributes of the PolicyCheck instance for debugging purposes. + """ + if self.debug: + self.print_stderr(f'Policy: {self.name}') + self.print_stderr(f'Format: {self.format_type}') + self.print_stderr(f'Status: {self.status}') + self.print_stderr(f'Output: {self.output}') + + def _is_valid_format(self) -> bool: + """ + Validate if the format specified is supported. + + This method checks if the format stored in format is one of the + valid formats defined in self.VALID_FORMATS. + + :return: bool: True if the format is valid, False otherwise. + """ + if self.format_type not in self.VALID_FORMATS: + valid_formats_str = ', '.join(self.VALID_FORMATS) + self.print_stderr(f'ERROR: Invalid format "{self.format_type}". Valid formats are: {valid_formats_str}') + return False + return True + + def _generate_formatter_report(self, components: list[Dict]): + """ + Generates a formatted report for a given component based on the defined formatter. + + Parameters: + components (List[dict]): A list of dictionaries representing the components to be + processed and formatted. Each dictionary contains detailed information that adheres + to the format requirements for the specified formatter. + + Returns: + Tuple[int, dict]: A tuple where the first element represents the policy status code + and the second element is a dictionary containing formatted results information, + typically with keys 'details' and 'summary'. + + Raises: + KeyError: When a required key is missing from the provided component, causing the + formatter to fail. + ValueError: If an invalid component is passed and renders unable to process. + """ + # Get a formatter for the output results + formatter = self._get_formatter() + if formatter is None: + return PolicyStatus.ERROR.value, {} + # Format the results + policy_output = formatter(components) + ## Save outputs if required + self.print_to_file_or_stdout(policy_output.details, self.output) + self.print_to_file_or_stderr(policy_output.summary, self.status) + # Check to see if we have policy violations + if len(components) > 0: + return PolicyStatus.POLICY_FAIL.value, policy_output + return PolicyStatus.POLICY_SUCCESS.value, policy_output +# +# End of PolicyCheck Class +# \ No newline at end of file diff --git a/src/scanoss/inspection/policy_check/scanoss/__init__.py b/src/scanoss/inspection/policy_check/scanoss/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/scanoss/inspection/policy_check/scanoss/copyleft.py b/src/scanoss/inspection/policy_check/scanoss/copyleft.py new file mode 100644 index 00000000..a56c39b9 --- /dev/null +++ b/src/scanoss/inspection/policy_check/scanoss/copyleft.py @@ -0,0 +1,243 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +import json +from dataclasses import dataclass +from typing import Dict, List + +from scanoss.constants import DEFAULT_COPYLEFT_LICENSE_SOURCES + +from ...policy_check.policy_check import PolicyCheck, PolicyOutput, PolicyStatus +from ...utils.markdown_utils import generate_jira_table, generate_table +from ...utils.scan_result_processor import ScanResultProcessor + + +@dataclass +class License: + spdxid: str + copyleft: bool + url: str + source: str + +@dataclass +class Component: + purl: str + version: str + licenses: List[License] + status: str + +class Copyleft(PolicyCheck[Component]): + """ + SCANOSS Copyleft class + Inspects components for copyleft licenses + """ + + def __init__( # noqa: PLR0913 + self, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + filepath: str = None, + format_type: str = 'json', + status: str = None, + output: str = None, + include: str = None, + exclude: str = None, + explicit: str = None, + license_sources: list = None, + ): + """ + Initialise the Copyleft class. + + :param debug: Enable debug mode + :param trace: Enable trace mode (default True) + :param quiet: Enable quiet mode + :param filepath: Path to the file containing component data + :param format_type: Output format ('json' or 'md') + :param status: Path to save the status output + :param output: Path to save detailed output + :param include: Licenses to include in the analysis + :param exclude: Licenses to exclude from the analysis + :param explicit: Explicitly defined licenses + :param license_sources: List of license sources to check + """ + super().__init__( + debug, trace, quiet, format_type, status, name='Copyleft Policy', output=output + ) + self.license_util.init(include, exclude, explicit) + self.filepath = filepath + self.output = output + self.status = status + self.license_sources = license_sources or DEFAULT_COPYLEFT_LICENSE_SOURCES + self.results_processor = ScanResultProcessor( + self.debug, + self.trace, + self.quiet, + self.filepath, + include, + exclude, + explicit, + self.license_sources) + + def _json(self, components: list[Component]) -> PolicyOutput: + """ + Format the components with copyleft licenses as JSON. + + :param components: List of components with copyleft licenses + :return: Dictionary with formatted JSON details and summary + """ + # A component is considered unique by its combination of PURL (Package URL) and license + component_licenses = self.results_processor.group_components_by_license(components) + details = {} + if len(components) > 0: + details = {'components': components} + return PolicyOutput( + details= f'{json.dumps(details, indent=2)}\n', + summary= f'{len(component_licenses)} component(s) with copyleft licenses were found.\n', + ) + + def _markdown(self, components: list[Component]) -> PolicyOutput: + """ + Format the components with copyleft licenses as Markdown. + + :param components: List of components with copyleft licenses + :return: Dictionary with formatted Markdown details and summary + """ + return self._md_summary_generator(components, generate_table) + + def _jira_markdown(self, components: list[Component]) -> PolicyOutput: + """ + Format the components with copyleft licenses as Markdown. + + :param components: List of components with copyleft licenses + :return: Dictionary with formatted Markdown details and summary + """ + return self._md_summary_generator(components, generate_jira_table) + + def _md_summary_generator(self, components: list[Component], table_generator) -> PolicyOutput: + """ + Generates a Markdown summary for components with a focus on copyleft licenses. + + This function processes a list of components and groups them by their licenses. + For each group, the components are mapped with their license data and a tabular representation is created. + The generated Markdown summary includes a detailed table and a summary overview. + + Parameters: + components: list[Component] + A list of Component objects to process for generating the summary. + table_generator + A callable function to generate tabular data for components. + + Returns: + PolicyOutput + """ + # A component is considered unique by its combination of PURL (Package URL) and license + component_licenses = self.results_processor.group_components_by_license(components) + headers = ['Component', 'License', 'URL', 'Copyleft'] + centered_columns = [1, 4] + rows = [] + for comp_lic_item in component_licenses: + row = [ + comp_lic_item['purl'], + comp_lic_item['spdxid'], + comp_lic_item['url'], + 'YES' if comp_lic_item['copyleft'] else 'NO', + ] + rows.append(row) + # End license loop + # End component loop + return PolicyOutput( + details= f'### Copyleft Licenses\n{table_generator(headers, rows, centered_columns)}', + summary= f'{len(component_licenses)} component(s) with copyleft licenses were found.\n', + ) + + def _get_components_with_copyleft_licenses(self, components: list) -> list[Dict]: + """ + Filter the components list to include only those with copyleft licenses. + + :param components: List of all components + :return: List of components with copyleft licenses + """ + filtered_components = [] + for component in components: + copyleft_licenses = [lic for lic in component['licenses'] if lic['copyleft']] + if copyleft_licenses: + # Remove unused keys + del component['count'] + del component['declared'] + del component['undeclared'] + filtered_component = component + # Remove 'count' from each license using pop + for lic in copyleft_licenses: + lic.pop('count', None) # None is default value if key doesn't exist + + filtered_component['licenses'] = copyleft_licenses + filtered_components.append(filtered_component) + # End component loop + self.print_debug(f'Copyleft components: {filtered_components}') + return filtered_components + + def _get_components(self): + """ + Extract and process components from results and their dependencies. + + This method performs the following steps: + 1. Validates that `self.results` is loaded. Returns `None` if not. + 2. Extracts file, snippet, and dependency components into a dictionary. + 3. Converts components to a list and processes their licenses. + + :return: A list of processed components with license data, or `None` if `self.results` is not set. + """ + if self.results_processor.get_results() is None: + return None + components: dict = {} + # Extract component and license data from file and dependency results. Both helpers mutate `components` + self.results_processor.get_components_data(components) + self.results_processor.get_dependencies_data(components) + return self.results_processor.convert_components_to_list(components) + + def run(self): + """ + Run the copyleft license inspection process. + + This method performs the following steps: + 1. Get all components + 2. Filter components with copyleft licenses + 3. Format the results + 4. Save the output to files if required + + :return: Dictionary containing the inspection results + """ + self._debug() + # Get the components from the results + components = self._get_components() + if components is None: + return PolicyStatus.ERROR.value, {} + # Get a list of copyleft components if they exist + copyleft_components = self._get_components_with_copyleft_licenses(components) + # Format the results and save to files if required + return self._generate_formatter_report(copyleft_components) +# +# End of Copyleft Class +# diff --git a/src/scanoss/inspection/policy_check/scanoss/undeclared_component.py b/src/scanoss/inspection/policy_check/scanoss/undeclared_component.py new file mode 100644 index 00000000..ce122a5b --- /dev/null +++ b/src/scanoss/inspection/policy_check/scanoss/undeclared_component.py @@ -0,0 +1,309 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +import json +from dataclasses import dataclass +from typing import List + +from ...policy_check.policy_check import PolicyCheck, PolicyOutput, PolicyStatus +from ...utils.markdown_utils import generate_jira_table, generate_table +from ...utils.scan_result_processor import ScanResultProcessor + + +@dataclass +class License: + spdxid: str + copyleft: bool + url: str + +@dataclass +class Component: + purl: str + version: str + licenses: List[License] + status: str + +class UndeclaredComponent(PolicyCheck[Component]): + """ + SCANOSS UndeclaredComponent class + Inspects for undeclared components + """ + + def __init__( # noqa: PLR0913 + self, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + filepath: str = None, + format_type: str = 'json', + status: str = None, + output: str = None, + sbom_format: str = 'settings' + ): + """ + Initialize the UndeclaredComponent class. + + :param debug: Enable debug mode + :param trace: Enable trace mode (default True) + :param quiet: Enable quiet mode + :param filepath: Path to the file containing component data + :param format_type: Output format ('json' or 'md') + :param status: Path to save status output + :param output: Path to save detailed output + :param sbom_format: Sbom format for status output (default 'settings') + """ + super().__init__( + debug, trace, quiet, format_type, status, name='Undeclared Components Policy', output=output + ) + self.filepath = filepath + self.output = output + self.status = status + self.sbom_format = sbom_format + self.results_processor = ScanResultProcessor(self.debug, self.trace, self.quiet, self.filepath) + + + def _get_undeclared_components(self, components: list[Component]) -> list or None: + """ + Filter the components list to include only undeclared components. + + :param components: List of all components + :return: List of undeclared components + """ + if components is None: + self.print_debug('WARNING: No components provided!') + return None + undeclared_components = [] + for component in components: + if component['status'] == 'pending': + # Remove unused keys + del component['count'] + del component['declared'] + del component['undeclared'] + for lic in component['licenses']: + lic.pop('count', None) # None is default value if key doesn't exist + lic.pop('source', None) # None is default value if key doesn't exist + undeclared_components.append(component) + # end component loop + return undeclared_components + + def _get_jira_summary(self, components: list[Component]) -> str: + """ + Get a summary of the undeclared components. + + :param components: List of all components + :return: Component summary markdown + """ + + """ + Get a summary of the undeclared components. + + :param components: List of all components + :return: Component summary markdown + """ + if len(components) > 0: + json_content = json.dumps(self._generate_scanoss_file(components), indent=2) + + if self.sbom_format == 'settings': + return ( + f'{len(components)} undeclared component(s) were found.\n' + f'Add the following snippet into your `scanoss.json` file\n' + f'{{code:json}}\n' + f'{json_content}\n' + f'{{code}}\n' + ) + else: + return ( + f'{len(components)} undeclared component(s) were found.\n' + f'Add the following snippet into your `sbom.json` file\n' + f'{{code:json}}\n' + f'{json_content}\n' + f'{{code}}\n' + ) + return f'{len(components)} undeclared component(s) were found.\\n' + + def _get_summary(self, components: list) -> str: + """ + Get a summary of the undeclared components. + + :param components: List of all components + :return: Component summary markdown + """ + summary = f'{len(components)} undeclared component(s) were found.\n' + if len(components) > 0: + if self.sbom_format == 'settings': + summary += ( + f'Add the following snippet into your `scanoss.json` file\n' + f'\n```json\n{json.dumps(self._generate_scanoss_file(components), indent=2)}\n```\n' + ) + else: + summary += ( + f'Add the following snippet into your `sbom.json` file\n' + f'\n```json\n{json.dumps(self._generate_sbom_file(components), indent=2)}\n```\n' + ) + + return summary + + def _json(self, components: list[Component]) -> PolicyOutput: + """ + Format the undeclared components as JSON. + + :param components: List of undeclared components + :return: Dictionary with formatted JSON details and summary + """ + # Use component grouped by licenses to generate the summary + component_licenses = self.results_processor.group_components_by_license(components) + details = {} + if len(components) > 0: + details = {'components': components} + return PolicyOutput( + details=f'{json.dumps(details, indent=2)}\n', + summary=self._get_summary(component_licenses) + ) + + def _markdown(self, components: list[Component]) -> PolicyOutput: + """ + Format the undeclared components as Markdown. + + :param components: List of undeclared components + :return: Dictionary with formatted Markdown details and summary + """ + headers = ['Component', 'License'] + rows = [] + # TODO look at using SpdxLite license name lookup method + component_licenses = self.results_processor.group_components_by_license(components) + for component in component_licenses: + rows.append([component.get('purl'), component.get('spdxid')]) + return PolicyOutput( + details= f'### Undeclared components\n{generate_table(headers, rows)}\n', + summary= self._get_summary(component_licenses), + ) + + def _jira_markdown(self, components: list) -> PolicyOutput: + """ + Format the undeclared components as Markdown. + + :param components: List of undeclared components + :return: Dictionary with formatted Markdown details and summary + """ + headers = ['Component', 'License'] + rows = [] + # TODO look at using SpdxLite license name lookup method + component_licenses = self.results_processor.group_components_by_license(components) + for component in component_licenses: + rows.append([component.get('purl'), component.get('spdxid')]) + return PolicyOutput( + details= f'{generate_jira_table(headers, rows)}', + summary= self._get_jira_summary(component_licenses), + ) + + def _get_unique_components(self, components: list) -> list: + """ + Generate a list of unique components. + + :param components: List of undeclared components + :return: list of unique components + """ + unique_components = {} + if components is None: + self.print_stderr('WARNING: No components provided!') + return [] + + for component in components: + unique_components[component['purl']] = {'purl': component['purl']} + return list(unique_components.values()) + + def _generate_scanoss_file(self, components: list) -> dict: + """ + Generate a list of PURLs for the scanoss.json file. + + :param components: List of undeclared components + :return: scanoss.json Dictionary + """ + scanoss_settings = { + 'bom': { + 'include': self._get_unique_components(components), + } + } + + return scanoss_settings + + def _generate_sbom_file(self, components: list) -> dict: + """ + Generate a list of PURLs for the SBOM file. + + :param components: List of undeclared components + :return: SBOM Dictionary with components + """ + sbom = { + 'components': self._get_unique_components(components), + } + + return sbom + + def _get_components(self): + """ + Extract and process components from file results only. + + This method performs the following steps: + 1. Validates if `self.results` is loaded. Returns `None` if not loaded. + 2. Extracts file and snippet components into a dictionary. + 3. Converts the components dictionary into a list of components. + 4. Processes the licenses for each component by converting them into a list. + + :return: A list of processed components with their licenses, or `None` if `self.results` is not set. + """ + if self.results_processor.get_results() is None: + return None + components: dict = {} + # Extract file and snippet components + components = self.results_processor.get_components_data(components) + # Convert to list and process licenses + return self.results_processor.convert_components_to_list(components) + + def run(self): + """ + Run the undeclared component inspection process. + + This method performs the following steps: + 1. Get all components + 2. Filter undeclared components + 3. Format the results + 4. Save the output to files if required + + :return: Dictionary containing the inspection results + """ + self._debug() + components = self._get_components() + if components is None: + return PolicyStatus.ERROR.value, {} + # Get an undeclared component summary (if any) + undeclared_components = self._get_undeclared_components(components) + if undeclared_components is None: + return PolicyStatus.ERROR.value, {} + self.print_debug(f'Undeclared components: {undeclared_components}') + # Format the results and save to files if required + return self._generate_formatter_report(undeclared_components) +# +# End of UndeclaredComponent Class +# diff --git a/src/scanoss/inspection/summary/__init__.py b/src/scanoss/inspection/summary/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/scanoss/inspection/summary/component_summary.py b/src/scanoss/inspection/summary/component_summary.py new file mode 100644 index 00000000..b4563176 --- /dev/null +++ b/src/scanoss/inspection/summary/component_summary.py @@ -0,0 +1,170 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" +import json +from typing import Any + +from ...scanossbase import ScanossBase +from ..policy_check.policy_check import T +from ..utils.scan_result_processor import ScanResultProcessor + + +class ComponentSummary(ScanossBase): + + def __init__( # noqa: PLR0913 + self, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + filepath: str = None, + format_type: str = 'json', + output: str = None, + ): + """ + Initialize the ComponentSummary class. + + :param debug: Enable debug mode + :param trace: Enable trace mode + :param quiet: Enable quiet mode + :param filepath: Path to the file containing component data + :param format_type: Output format ('json' or 'md') + """ + super().__init__(debug, trace, quiet) + self.filepath = filepath + self.output = output + self.results_processor = ScanResultProcessor(debug, trace, quiet, filepath) + + + def _json(self, data: dict[str,Any]) -> dict[str,Any]: + """ + Format component summary data as JSON. + + This method returns the component summary data in its original JSON structure + without any transformation. The data can be directly serialized to JSON format. + + :param data: Dictionary containing component summary information including: + - components: List of component-license pairs with status and metadata + - totalComponents: Total number of unique components + - undeclaredComponents: Number of components with 'pending' status + - declaredComponents: Number of components with 'identified' status + - totalFilesDetected: Total count of files where components were detected + - totalFilesUndeclared: Count of files with undeclared components + - totalFilesDeclared: Count of files with declared components + :return: The same data dictionary, ready for JSON serialization + """ + return data + + def _markdown(self, data: list[T]) -> dict[str, Any]: + """ + Format component summary data as Markdown (not yet implemented). + + This method is intended to convert component summary data into a human-readable + Markdown format with tables and formatted sections. + + :param data: List of component summary items to format + :return: Dictionary containing formatted Markdown output + """ + pass + + def _jira_markdown(self, data: list[T]) -> dict[str, Any]: + """ + Format component summary data as Jira-flavored Markdown (not yet implemented). + + This method is intended to convert component summary data into Jira-compatible + Markdown format, which may include Jira-specific syntax for tables and formatting. + + :param data: List of component summary items to format + :return: Dictionary containing Jira-formatted Markdown output + """ + pass + + def _get_component_summary_from_components(self, scan_components: list)-> dict: + """ + Get a component summary from detected components. + + :param scan_components: List of all components + :return: Dict with license summary information + """ + # A component is considered unique by its combination of PURL (Package URL) and license + component_licenses = self.results_processor.group_components_by_license(scan_components) + total_components = len(component_licenses) + # Get undeclared components + undeclared_components = len([c for c in component_licenses if c['status'] == 'pending']) + + components: list = [] + total_undeclared_files = 0 + total_files_detected = 0 + for component in scan_components: + total_files_detected += component['count'] + total_undeclared_files += component['undeclared'] + components.append({ + 'purl': component['purl'], + 'version': component['version'], + 'count': component['count'], + 'undeclared': component['undeclared'], + 'declared': component['count'] - component['undeclared'], + }) + ## End for loop components + return { + "components": component_licenses, + 'totalComponents': total_components, + 'undeclaredComponents': undeclared_components, + 'declaredComponents': total_components - undeclared_components, + 'totalFilesDetected': total_files_detected, + 'totalFilesUndeclared': total_undeclared_files, + 'totalFilesDeclared': total_files_detected - total_undeclared_files, + } + + def _get_components(self): + """ + Extract and process components from results and their dependencies. + + This method performs the following steps: + 1. Validates that `self.results` is loaded. Returns `None` if not. + 2. Extracts file, snippet, and dependency components into a dictionary. + 3. Converts components to a list and processes their licenses. + + :return: A list of processed components with license data, or `None` if `self.results` is not set. + """ + if self.results_processor.get_results() is None: + raise ValueError(f'Error: No results found in {self.filepath}') + + components: dict = {} + # Extract component and license data from file and dependency results. Both helpers mutate `components` + self.results_processor.get_components_data(components) + return self.results_processor.convert_components_to_list(components) + + def _format(self, component_summary) -> str: + # TODO: Implement formatter to support dynamic outputs + json_data = self._json(component_summary) + return json.dumps(json_data, indent=2) + + def run(self): + components = self._get_components() + component_summary = self._get_component_summary_from_components(components) + output = self._format(component_summary) + self.print_to_file_or_stdout(output, self.output) + return component_summary +# +# End of ComponentSummary Class +# \ No newline at end of file diff --git a/src/scanoss/inspection/summary/license_summary.py b/src/scanoss/inspection/summary/license_summary.py new file mode 100644 index 00000000..3cf3761e --- /dev/null +++ b/src/scanoss/inspection/summary/license_summary.py @@ -0,0 +1,191 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +import json +from typing import Any + +from ...scanossbase import ScanossBase +from ..policy_check.policy_check import T +from ..utils.scan_result_processor import ScanResultProcessor + + +class LicenseSummary(ScanossBase): + """ + SCANOSS LicenseSummary class + Inspects results and generates comprehensive license summaries from detected components. + + This class processes component scan results to extract, validate, and aggregate license + information, providing detailed summaries including copyleft analysis and license statistics. + """ + + # Define required license fields as class constants + REQUIRED_LICENSE_FIELDS = ['spdxid', 'url', 'copyleft', 'source'] + + def __init__( # noqa: PLR0913 + self, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + filepath: str = None, + status: str = None, + output: str = None, + include: str = None, + exclude: str = None, + explicit: str = None, + ): + """ + Initialize the LicenseSummary class. + + :param debug: Enable debug mode + :param trace: Enable trace mode + :param quiet: Enable quiet mode + :param filepath: Path to the file containing component data + :param output: Path to save detailed output + :param include: Licenses to include in the analysis + :param exclude: Licenses to exclude from the analysis + :param explicit: Explicitly defined licenses + """ + super().__init__(debug=debug, trace=trace, quiet=quiet) + self.results_processor = ScanResultProcessor(debug, trace, quiet, filepath, include, exclude, explicit) + self.filepath = filepath + self.output = output + self.status = status + self.include = include + self.exclude = exclude + self.explicit = explicit + + def _json(self, data: dict[str,Any]) -> dict[str, Any]: + """ + Format license summary data as JSON. + + This method is intended to return the license summary data in JSON structure + for serialization. The data should include license information with copyleft + analysis and license statistics. + + :param data: List of license summary items to format + :return: Dictionary containing license summary information including: + - licenses: List of detected licenses with SPDX IDs, URLs, and copyleft status + - detectedLicenses: Total number of unique licenses + - detectedLicensesWithCopyleft: Count of licenses marked as copyleft + """ + return data + + def _markdown(self, data: list[T]) -> dict[str, Any]: + """ + Format license summary data as Markdown (not yet implemented). + + This method is intended to convert license summary data into a human-readable + Markdown format with tables and formatted sections. + + :param data: List of license summary items to format + :return: Dictionary containing formatted Markdown output + """ + pass + + def _jira_markdown(self, data: list[T]) -> dict[str, Any]: + """ + Format license summary data as Jira-flavored Markdown (not yet implemented). + + This method is intended to convert license summary data into Jira-compatible + Markdown format, which may include Jira-specific syntax for tables and formatting. + + :param data: List of license summary items to format + :return: Dictionary containing Jira-formatted Markdown output + """ + pass + + + def _get_licenses_summary_from_components(self, components: list)-> dict: + """ + Get a license summary from detected components. + + :param components: List of all components + :return: Dict with license summary information + """ + # A component is considered unique by its combination of PURL (Package URL) and license + component_licenses = self.results_processor.group_components_by_license(components) + license_component_count = {} + # Count license per component + for lic in component_licenses: + if lic['spdxid'] not in license_component_count: + license_component_count[lic['spdxid']] = 1 + else: + license_component_count[lic['spdxid']] += 1 + licenses:dict = {} + for comp_lic in component_licenses: + spdxid = comp_lic.get("spdxid") + url = comp_lic.get("url") + copyleft = comp_lic.get("copyleft") + if spdxid not in licenses: + licenses[spdxid] = { + 'spdxid': spdxid, + 'url': url, + 'copyleft': copyleft, + 'componentCount': license_component_count.get(spdxid, 0), # Append component count to license + } + ## End for loop licenses + ## End for loop components + detected_licenses = list(licenses.values()) + licenses_with_copyleft = [lic for lic in detected_licenses if lic['copyleft']] + return { + 'licenses': detected_licenses, + 'detectedLicenses': len(detected_licenses), # Count unique licenses. SPDXID is considered unique + 'detectedLicensesWithCopyleft': len(licenses_with_copyleft), + } + + + def _get_components(self): + """ + Extract and process components from results and their dependencies. + + This method performs the following steps: + 1. Validates that `self.results` is loaded. Returns `None` if not. + 2. Extracts file, snippet, and dependency components into a dictionary. + 3. Converts components to a list and processes their licenses. + + :return: A list of processed components with license data, or `None` if `self.results` is not set. + """ + if self.results_processor.get_results() is None: + raise ValueError(f'Error: No results found in {self.filepath}') + + components: dict = {} + # Extract component and license data from file and dependency results. Both helpers mutate `components` + self.results_processor.get_components_data(components) + self.results_processor.get_dependencies_data(components) + return self.results_processor.convert_components_to_list(components) + + def _format(self, license_summary) -> str: + # TODO: Implement formatter to support dynamic outputs + json_data = self._json(license_summary) + return json.dumps(json_data, indent=2) + + def run(self): + components = self._get_components() + license_summary = self._get_licenses_summary_from_components(components) + output = self._format(license_summary) + self.print_to_file_or_stdout(output, self.output) + return license_summary +# +# End of LicenseSummary Class +# \ No newline at end of file diff --git a/src/scanoss/inspection/summary/match_summary.py b/src/scanoss/inspection/summary/match_summary.py new file mode 100644 index 00000000..b79233e5 --- /dev/null +++ b/src/scanoss/inspection/summary/match_summary.py @@ -0,0 +1,341 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +from dataclasses import dataclass + +from ...scanossbase import ScanossBase +from ...utils import scanoss_scan_results_utils +from ..utils.file_utils import load_json_file +from ..utils.markdown_utils import generate_table + + +@dataclass +class MatchSummaryItem: + """ + Represents a single match entry in the SCANOSS results. + + This data class encapsulates all the relevant information about a component + match found during scanning, including file location, license details, and + match quality metrics. + """ + file: str + file_url: str + license: str + similarity: str + purl: str + purl_url: str + version: str + lines: str + + +@dataclass +class ComponentMatchSummary: + """ + Container for categorized SCANOSS match results. + + Organizes matches into two categories: full file matches and snippet matches. + This separation allows for different presentation and analysis of match types. + """ + files: list[MatchSummaryItem] + snippet: list[MatchSummaryItem] + +class MatchSummary(ScanossBase): + """ + Generates Markdown summaries from SCANOSS scan results. + + This class processes SCANOSS scan results and creates human-readable Markdown + reports with collapsible sections for file and snippet matches. The reports + include clickable links to files when a line range + prefix is provided. + """ + + def __init__( # noqa: PLR0913 + self, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + line_range_prefix: str = None, + scanoss_results_path: str = None, + output: str = None, + ): + """ + Initialize the Matches Summary generator. + + :param debug: Enable debug output for troubleshooting + :param trace: Enable trace-level logging for detailed execution tracking + :param quiet: Suppress informational messages + :param line_range_prefix: Base URL prefix for GitLab file links with line ranges + (e.g., 'https://gitlab.com/org/project/-/blob/main') + :param scanoss_results_path: Path to SCANOSS scan results file in JSON format + :param output: Output file path for the generated Markdown report (default: stdout) + """ + super().__init__(debug=debug, trace=trace, quiet=quiet) + self.scanoss_results_path = scanoss_results_path + self.line_range_prefix = line_range_prefix + self.output = output + self.print_debug("Initializing MatchSummary class") + + + def _get_match_summary_item(self, file_name: str, result: dict) -> MatchSummaryItem: + """ + Create a MatchSummaryItem from a single scan result. + + Processes a SCANOSS scan result and creates a MatchSummaryItem with appropriate + file URLs, license information, and line ranges. Handles both snippet matches + (with specific line ranges) and file matches (entire file). + + :param file_name: Name of the scanned file (relative path in the repository) + :param result: SCANOSS scan result dictionary containing match details + :return: Populated match summary item with all relevant information + """ + self.print_trace(f"Creating match summary item for file: {file_name}, id: {result.get('id')}") + + if result.get('id') == "snippet": + # Snippet match: create URL with line range anchor + lines = scanoss_scan_results_utils.get_lines(result.get('lines')) + end_line = lines[len(lines) - 1] if len(lines) > 1 else lines[0] + file_url = f"{self.line_range_prefix}/{file_name}#L{lines[0]}-L{end_line}" + + self.print_trace(f"Snippet match: lines {lines[0]}-{end_line}, purl: {result.get('purl')[0]}") + + return MatchSummaryItem( + file_url=file_url, + file=file_name, + license=result.get('licenses')[0].get('name'), + similarity=result.get('matched'), + purl=result.get('purl')[0], + purl_url=result.get('url'), + version=result.get('version'), + lines=f"{lines[0]}-{lines[len(lines) - 1] if len(lines) > 1 else lines[0]}" + ) + # File match: create URL without line range + self.print_trace(f"File match: {file_name}, purl: {result.get('purl')[0]}, version: {result.get('version')}") + + return MatchSummaryItem( + file=file_name, + file_url=f"{self.line_range_prefix}/{file_name}", + license=result.get('licenses')[0].get('name'), + similarity=result.get('matched'), + purl=result.get('purl')[0], + purl_url=result.get('url'), + version=result.get('version'), + lines="all" + ) + + def _validate_result(self, file_name: str, result: dict) -> bool: + """ + Validate that a scan result has all required fields. + + :param file_name: Name of the file being validated + :param result: The scan result to validate + :return: True if valid, False otherwise + """ + validations = [ + ('id', 'No id found'), + ('lines', 'No lines found'), + ('purl', 'No purl found'), + ('licenses', 'No licenses found'), + ('version', 'No version found'), + ('matched', 'No matched found'), + ('url', 'No url found'), + ] + + for field, error_msg in validations: + if not result.get(field): + self.print_debug(f'ERROR: {error_msg} for file {file_name}') + return False + + # Additional validation for non-empty lists + if len(result.get('purl')) == 0: + self.print_debug(f'ERROR: No purl found for file {file_name}') + return False + if len(result.get('licenses')) == 0: + self.print_debug(f'ERROR: Empty licenses list for file {file_name}') + return False + + return True + + def _get_matches_summary(self) -> ComponentMatchSummary: + """ + Parse SCANOSS scan results and create categorized match summaries. + + Loads the SCANOSS scan results file and processes each match, validating + required fields and categorizing matches into file matches and snippet matches. + Skips invalid or incomplete results with debug messages. + """ + self.print_debug(f"Loading scan results from: {self.scanoss_results_path}") + + # Load scan results from JSON file + scan_results = load_json_file(self.scanoss_results_path) + gitlab_matches_summary = ComponentMatchSummary(files=[], snippet=[]) + + self.print_debug(f"Processing {len(scan_results)} files from scan results") + self.print_trace(f"Line range prefix set to: {self.line_range_prefix}") + + # Process each file and its results + for file_name, results in scan_results.items(): + self.print_trace(f"Processing file: {file_name} with {len(results)} results") + + for result in results: + # Skip non-matches + if result.get('id') == "none": + self.print_debug(f'Skipping non-match for file {file_name}') + continue + + # Validate required fields + if not self._validate_result(file_name, result): + continue + + # Create summary item and categorize by match type + summary_item = self._get_match_summary_item(file_name, result) + if result.get('id') == "snippet": + gitlab_matches_summary.snippet.append(summary_item) + self.print_trace(f"Added snippet match for {file_name}") + else: + gitlab_matches_summary.files.append(summary_item) + self.print_trace(f"Added file match for {file_name}") + + self.print_debug( + f"Match summary complete: {len(gitlab_matches_summary.files)} file matches, " + f"{len(gitlab_matches_summary.snippet)} snippet matches" + ) + + return gitlab_matches_summary + + + def _markdown(self, gitlab_matches_summary: ComponentMatchSummary) -> str: + """ + Generate Markdown from match summaries. + + Creates a formatted Markdown document with collapsible sections for file + and snippet matches. + + :param gitlab_matches_summary: Container with categorized file and snippet matches to format + :return: Complete Markdown document with formatted match tables + """ + self.print_debug("Generating Markdown from match summaries") + + if len(gitlab_matches_summary.files) == 0 and len(gitlab_matches_summary.snippet) == 0: + self.print_debug("No matches to format - returning empty string") + return "" + + self.print_trace( + f"Formatting {len(gitlab_matches_summary.files)} file matches and " + f"{len(gitlab_matches_summary.snippet)} snippet matches" + ) + + # Define table headers + file_match_headers = ['File', 'License', 'Similarity', 'PURL', 'Version'] + snippet_match_headers = ['File', 'License', 'Similarity', 'PURL', 'Version', 'Lines'] + + # Build file matches table + self.print_trace("Building file matches table") + file_match_rows = [] + for file_match in gitlab_matches_summary.files: + row = [ + f"[{file_match.file}]({file_match.file_url})", + file_match.license, + file_match.similarity, + f"[{file_match.purl}]({file_match.purl_url})", + file_match.version, + ] + file_match_rows.append(row) + file_match_table = generate_table(file_match_headers, file_match_rows) + + # Build snippet matches table + self.print_trace("Building snippet matches table") + snippet_match_rows = [] + for snippet_match in gitlab_matches_summary.snippet: + row = [ + f"[{snippet_match.file}]({snippet_match.file_url})", + snippet_match.license, + snippet_match.similarity, + f"[{snippet_match.purl}]({snippet_match.purl_url})", + snippet_match.version, + snippet_match.lines + ] + snippet_match_rows.append(row) + snippet_match_table = generate_table(snippet_match_headers, snippet_match_rows) + + # Assemble complete Markdown document + markdown = "" + markdown += "### SCANOSS Match Summary\n\n" + + # File matches section (collapsible) + markdown += "
\n" + markdown += "File Match Summary\n\n" + markdown += file_match_table + markdown += "\n
\n" + + # Snippet matches section (collapsible) + markdown += "
\n" + markdown += "Snippet Match Summary\n\n" + markdown += snippet_match_table + markdown += "\n
\n" + + self.print_trace(f"Markdown generation complete (length: {len(markdown)} characters)") + self.print_debug("Match summary Markdown generation complete") + return markdown + + def run(self): + """ + Execute the matches summary generation process. + + This is the main entry point for generating the matches summary report. + It orchestrates the entire workflow: + 1. Loads and parses SCANOSS scan results + 2. Validates and categorizes matches + 3. Generates Markdown report + 4. Outputs to file or stdout + """ + self.print_debug("Starting match summary generation process") + self.print_trace( + f"Configuration - Results path: {self.scanoss_results_path}, Output: {self.output}, " + f"Line range prefix: {self.line_range_prefix}" + ) + + # Load and process scan results into categorized matches + self.print_trace("Loading and processing scan results") + matches = self._get_matches_summary() + + # Format matches as GitLab-compatible Markdown + self.print_trace("Generating Markdown output") + matches_md = self._markdown(matches) + if matches_md == "": + self.print_debug("No matches found - exiting") + self.print_stdout("No matches found.") + return + + # Output to file or stdout + self.print_trace("Writing output") + if self.output: + self.print_debug(f"Writing match summary to file: {self.output}") + else: + self.print_debug("Writing match summary to 'stdout'") + + self.print_to_file_or_stdout(matches_md, self.output) + self.print_debug("Match summary generation complete") + + + diff --git a/src/scanoss/inspection/utils/file_utils.py b/src/scanoss/inspection/utils/file_utils.py new file mode 100644 index 00000000..a7e5de41 --- /dev/null +++ b/src/scanoss/inspection/utils/file_utils.py @@ -0,0 +1,44 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +import json +import os + + +def load_json_file(file_path: str) -> dict: + """ + Load the file + + :param file_path: file path to the JSON file + + Returns: + Dict[str, Any]: The parsed JSON data + """ + if not os.path.exists(file_path): + raise ValueError(f'The file "{file_path}" does not exist.') + with open(file_path, 'r') as jsonfile: + try: + return json.load(jsonfile) + except Exception as e: + raise ValueError(f'ERROR: Problem parsing input JSON: {e}') \ No newline at end of file diff --git a/src/scanoss/inspection/utils/license_utils.py b/src/scanoss/inspection/utils/license_utils.py new file mode 100644 index 00000000..fd4fec38 --- /dev/null +++ b/src/scanoss/inspection/utils/license_utils.py @@ -0,0 +1,123 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2024, SCANOSS + + 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. +""" + +from scanoss.osadl import Osadl + +from ...scanossbase import ScanossBase + + +class LicenseUtil(ScanossBase): + """ + A utility class for handling software licenses, particularly copyleft licenses. + + Uses OSADL (Open Source Automation Development Lab) authoritative copyleft data + with optional include/exclude/explicit filters. + """ + + BASE_SPDX_ORG_URL = 'https://spdx.org/licenses' + + def __init__(self, debug: bool = False, trace: bool = True, quiet: bool = False): + super().__init__(debug, trace, quiet) + self.osadl = Osadl(debug=debug, trace=trace, quiet=quiet) + self.include_licenses = set() + self.exclude_licenses = set() + self.explicit_licenses = set() + + def init(self, include: str = None, exclude: str = None, explicit: str = None): + """ + Initialize copyleft license filters. + + :param include: Comma-separated licenses to mark as copyleft (in addition to OSADL) + :param exclude: Comma-separated licenses to mark as NOT copyleft (override OSADL) + :param explicit: Comma-separated licenses to use exclusively (ignore OSADL) + """ + # Reset previous filters so init() can be safely called multiple times + self.include_licenses.clear() + self.exclude_licenses.clear() + self.explicit_licenses.clear() + + # Parse explicit list (if provided, ignore OSADL completely) + if explicit: + self.explicit_licenses = {lic.strip().lower() for lic in explicit.split(',') if lic.strip()} + self.print_debug(f'Explicit copyleft licenses: {self.explicit_licenses}') + return + + # Parse include list (mark these as copyleft in addition to OSADL) + if include: + self.include_licenses = {lic.strip().lower() for lic in include.split(',') if lic.strip()} + self.print_debug(f'Include licenses: {self.include_licenses}') + + # Parse exclude list (mark these as NOT copyleft, overriding OSADL) + if exclude: + self.exclude_licenses = {lic.strip().lower() for lic in exclude.split(',') if lic.strip()} + self.print_debug(f'Exclude licenses: {self.exclude_licenses}') + + def is_copyleft(self, spdxid: str) -> bool: + """ + Check if a license is copyleft. + + Logic: + 1. If explicit list provided → check if license in explicit list + 2. If license in include list → return True + 3. If license in exclude list → return False + 4. Otherwise → use OSADL authoritative data + + :param spdxid: SPDX license identifier + :return: True if copyleft, False otherwise + """ + if not spdxid: + self.print_debug('No license ID provided for copyleft check') + return False + + spdxid_lc = spdxid.lower() + + # Explicit mode: use only the explicit list + if self.explicit_licenses: + return spdxid_lc in self.explicit_licenses + + # Include filter: if license in include list, force copyleft=True + if spdxid_lc in self.include_licenses: + return True + + # Exclude filter: if license in exclude list, force copyleft=False + if spdxid_lc in self.exclude_licenses: + return False + + # No filters matched, use OSADL authoritative data + return self.osadl.is_copyleft(spdxid) + + def get_spdx_url(self, spdxid: str) -> str: + """ + Generate the URL for the SPDX page of a license. + + :param spdxid: The SPDX identifier of the license + :return: The URL of the SPDX page for the given license + """ + return f'{self.BASE_SPDX_ORG_URL}/{spdxid}.html' + + + +# +# End of LicenseUtil Class +# diff --git a/src/scanoss/inspection/utils/markdown_utils.py b/src/scanoss/inspection/utils/markdown_utils.py new file mode 100644 index 00000000..0ce47a26 --- /dev/null +++ b/src/scanoss/inspection/utils/markdown_utils.py @@ -0,0 +1,63 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +def generate_table(headers, rows, centered_columns=None): + """ + Generate a Markdown table. + + :param headers: List of headers for the table. + :param rows: List of rows for the table. + :param centered_columns: List of column indices to be centered. + :return: A string representing the Markdown table. + """ + col_sep = ' | ' + centered_column_set = set(centered_columns or []) + if headers is None: + return None + + # Decide which separator to use + def create_separator(index): + if centered_columns is None: + return '-' + return ':-:' if index in centered_column_set else '-' + + # Build the row separator + row_separator = col_sep + col_sep.join(create_separator(index) for index, _ in enumerate(headers)) + col_sep + # build table rows + table_rows = [col_sep + col_sep.join(headers) + col_sep, row_separator] + table_rows.extend(col_sep + col_sep.join(row) + col_sep for row in rows) + return '\n'.join(table_rows) + +def generate_jira_table(headers, rows, centered_columns=None): + col_sep = '*|*' + if headers is None: + return None + + table_header = '|*' + col_sep.join(headers) + '*|\n' + table = table_header + for row in rows: + if len(headers) == len(row): + table += '|' + '|'.join(row) + '|\n' + + return table \ No newline at end of file diff --git a/src/scanoss/inspection/utils/scan_result_processor.py b/src/scanoss/inspection/utils/scan_result_processor.py new file mode 100644 index 00000000..75960eab --- /dev/null +++ b/src/scanoss/inspection/utils/scan_result_processor.py @@ -0,0 +1,417 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +from enum import Enum +from typing import Any, Dict, TypeVar + +from ...scanossbase import ScanossBase +from ..utils.file_utils import load_json_file +from ..utils.license_utils import LicenseUtil + + +class ComponentID(Enum): + """ + Enumeration representing different types of software components. + + Attributes: + FILE (str): Represents a file component (value: "file"). + SNIPPET (str): Represents a code snippet component (value: "snippet"). + DEPENDENCY (str): Represents a dependency component (value: "dependency"). + """ + + FILE = 'file' + SNIPPET = 'snippet' + DEPENDENCY = 'dependency' + + +# +# End of ComponentID Class +# + +T = TypeVar('T') +class ScanResultProcessor(ScanossBase): + """ + A utility class for processing and transforming scan results. + + This class provides functionality for processing scan results, including methods for + loading, parsing, extracting, and aggregating component and license data from scan results. + It serves as a shared data processing layer used by both policy checks and summary generators. + + Inherits from: + ScanossBase: A base class providing common functionality for SCANOSS-related operations. + """ + + def __init__( # noqa: PLR0913 + self, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + result_file_path: str = None, + include: str = None, + exclude: str = None, + explicit: str = None, + license_sources: list = None, + ): + super().__init__(debug, trace, quiet) + self.result_file_path = result_file_path + self.license_util = LicenseUtil() + self.license_util.init(include, exclude, explicit) + self.license_sources = license_sources + self.results = self._load_input_file() + + def get_results(self) -> Dict[str, Any]: + return self.results + + def _append_component(self, components: Dict[str, Any], new_component: Dict[str, Any]) -> Dict[str, Any]: + """ + Append a new component to the component dictionary. + + This function creates a new entry in the component dictionary for the given component, + initializing all required counters: + - count: Total occurrences of this component (used by both license and component summaries) + - declared: Number of times this component is marked as 'identified' (used by component summary) + - undeclared: Number of times this component is marked as 'pending' (used by component summary) + + Each component also contains a 'licenses' dictionary where each license entry tracks: + - count: Number of times this license appears for this component (used by license summary) + + Args: + components: The existing dictionary of components + new_component: The new component to be added + Returns: + The updated components dictionary + """ + match_id = new_component.get('id') + # Determine the component key and purl based on component type + if match_id in [ComponentID.FILE.value, ComponentID.SNIPPET.value]: + purl = new_component['purl'][0] # Take the first purl for these component types + else: + purl = new_component['purl'] + + if not purl: + self.print_debug(f'WARNING: _append_component: No purl found for new component: {new_component}') + return components + + component_key = f'{purl}@{new_component["version"]}' + status = new_component.get('status') + + if component_key in components: + # Component already exists, update component counters and try to append a new license + self._update_component_counters(components[component_key], status) + self._append_license_to_component(components, new_component, component_key) + # Maintain 'pending' status - takes precedence over 'identified' + if status == 'pending': + components[component_key]['status'] = "pending" + return components + + # Create a new component + components[component_key] = { + 'purl': purl, + 'version': new_component['version'], + 'licenses': {}, + 'status': status, + 'count': 1, + 'declared': 1 if status == 'identified' else 0, + 'undeclared': 1 if status == 'pending' else 0 + } + + ## Append license to component + self._append_license_to_component(components, new_component, component_key) + return components + + def _append_license_to_component(self, + components: Dict[str, Any], new_component: Dict[str, Any], component_key: str) -> None: + """ + Add or update licenses for an existing component. + + For each license in the component: + - If the license already exists, increments its count + - If it's a new license, adds it with an initial count of 1 + + The license count is used by license_summary to track how many times each license appears + across all components. This count contributes to: + - Total number of licenses in the project + - Number of copyleft licenses when the license is marked as copyleft + + Args: + components: Dictionary containing all components + new_component: Component whose licenses need to be processed + component_key: purl + version of the component to be updated + """ + # If not licenses are present + if not new_component.get('licenses'): + self.print_debug(f'WARNING: Results missing licenses. Skipping: {new_component}') + return + + # Select licenses based on configuration (filtering or priority mode) + selected_licenses = self._select_licenses(new_component['licenses']) + + # Process licenses for this component + for license_item in selected_licenses: + if license_item.get('name'): + spdxid = license_item['name'] + source = license_item.get('source') + if not source: + source = 'unknown' + + if spdxid in components[component_key]['licenses']: + # If license exists, increment counter + components[component_key]['licenses'][spdxid]['count'] += 1 # Increment counter for license + else: + # If a license doesn't exist, create new entry + components[component_key]['licenses'][spdxid] = { + 'spdxid': spdxid, + 'copyleft': self.license_util.is_copyleft(spdxid), + 'url': self.license_util.get_spdx_url(spdxid), + 'source': source, + 'count': 1, # Set counter to 1 on new license + } + + def _update_component_counters(self, component, status): + """Update component counters based on status.""" + component['count'] += 1 + if status == 'identified': + component['declared'] += 1 + else: + component['undeclared'] += 1 + + def get_components_data(self, components: Dict[str, Any]) -> Dict[str, Any]: + """ + Extract and process file and snippet components from results. + + This method processes scan results to build or update component entries. For each component: + + Component Counters (used by ComponentSummary): + - count: Incremented for each occurrence of the component + - declared: Incremented when component status is 'identified' + - undeclared: Incremented when component status is 'pending' + + License Tracking: + - For new components, initializes license dictionary through _append_component + - For existing components, updates license counters through _append_license_to_component + which tracks the number of occurrences of each license + + Args: + components: A dictionary containing the raw results of a component scan + Returns: + Updated components dictionary with file and snippet data + """ + for component in self.results.values(): + for c in component: + component_id = c.get('id') + if not component_id: + self.print_debug(f'WARNING: Result missing id. Skipping: {c}') + continue + ## Skip dependency + if component_id == ComponentID.DEPENDENCY.value: + continue + status = c.get('status') + if not status: + self.print_debug(f'WARNING: Result missing status. Skipping: {c}') + continue + if component_id in [ComponentID.FILE.value, ComponentID.SNIPPET.value]: + if not c.get('purl'): + self.print_debug(f'WARNING: Result missing purl. Skipping: {c}') + continue + if len(c.get('purl')) <= 0: + self.print_debug(f'WARNING: Result missing purls. Skipping: {c}') + continue + version = c.get('version') + if not version: + self.print_debug(f'WARNING: Result missing version. Setting it to unknown: {c}') + version = 'unknown' + c['version'] = version #If no version exists. Set 'unknown' version to current component + # Append component + components = self._append_component(components, c) + + # End component loop + # End components loop + return components + + def get_dependencies_data(self,components: Dict[str, Any]) -> Dict[str, Any]: + """ + Extract and process dependency components from results. + :param components: Existing components dictionary to update + :return: Updated components dictionary with dependency data + """ + for component in self.results.values(): + for c in component: + component_id = c.get('id') + if not component_id: + self.print_debug(f'WARNING: Result missing id. Skipping: {c}') + continue + status = c.get('status') + if not status: + self.print_debug(f'WARNING: Result missing status. Skipping: {c}') + continue + if component_id == ComponentID.DEPENDENCY.value: + if c.get('dependencies') is None: + continue + for dependency in c['dependencies']: + if not dependency.get('purl'): + self.print_debug(f'WARNING: Dependency result missing purl. Skipping: {dependency}') + continue + version = dependency.get('version') + if not version: + self.print_debug(f'WARNING: Result missing version. Setting it to unknown: {c}') + version = 'unknown' + c['version'] = version # Set an 'unknown' version to the current component + + # Append component + components = self._append_component(components, dependency) + + # End dependency loop + # End component loop + # End of result loop + return components + + def _load_input_file(self): + """ + Load the result.json file + + Returns: + Dict[str, Any]: The parsed JSON data + """ + try: + return load_json_file(self.result_file_path) + except Exception as e: + self.print_stderr(f'ERROR: Problem parsing input JSON: {e}') + return None + + def convert_components_to_list(self, components: dict): + if components is None: + self.print_debug(f'WARNING: Components is empty {self.results}') + return None + results_list = list(components.values()) + for component in results_list: + licenses = component.get('licenses') + if licenses is not None: + component['licenses'] = list(licenses.values()) + else: + self.print_debug(f'WARNING: Licenses missing for: {component}') + component['licenses'] = [] + return results_list + + def _select_licenses(self, licenses_data): + """ + Select licenses based on configuration. + + Two modes: + - Filtering mode: If license_sources specified, filter to those sources + - Priority mode: Otherwise, use original priority-based selection + + Args: + licenses_data: List of license dictionaries + + Returns: + Filtered list of licenses based on configuration + """ + # Filtering mode, when license_sources is explicitly provided + if self.license_sources: + sources_to_include = set(self.license_sources) | {'unknown'} + return [lic for lic in licenses_data + if lic.get('source') in sources_to_include or lic.get('source') is None] + + # Define priority order (highest to lowest) + priority_sources = ['component_declared', 'license_file', 'file_header', 'scancode'] + + # Group licenses by source + licenses_by_source = {} + for license_item in licenses_data: + + source = license_item.get('source', 'unknown') + if source not in licenses_by_source: + licenses_by_source[source] = {} + + license_name = license_item.get('name') + if license_name: + # Use license name as key, store full license object as value + # If duplicate license names exist in same source, the last one wins + licenses_by_source[source][license_name] = license_item + + # Find the highest priority source that has licenses + for priority_source in priority_sources: + if priority_source in licenses_by_source: + self.print_trace(f'Choosing {priority_source} as source') + return list(licenses_by_source[priority_source].values()) + + # If no priority sources found, combine all licenses into a single list + self.print_debug("No priority sources found, returning all licenses as list") + return licenses_data + + def group_components_by_license(self,components): + """ + Groups components by their unique component-license pairs. + + This method processes a list of components and creates unique entries for each + component-license combination. If a component has multiple licenses, it will create + separate entries for each license. + + Args: + components: A list of component dictionaries. Each component should have: + - purl: Package URL identifying the component + - licenses: List of license dictionaries, each containing: + - spdxid: SPDX identifier for the license (optional) + + Returns: + list: A list of dictionaries, each containing: + - purl: The component's package URL + - license: The SPDX identifier of the license (or 'Unknown' if not provided) + """ + component_licenses: dict = {} + for component in components: + purl = component.get('purl', '') + status = component.get('status', '') + licenses = component.get('licenses', []) + + # Component without license + if not licenses: + key = f'{purl}-unknown' + component_licenses[key] = { + 'purl': purl, + 'spdxid': 'unknown', + 'status': status, + 'copyleft': False, + 'url': '-', + } + continue + + # Iterate over licenses component licenses + for lic in licenses: + spdxid = lic.get('spdxid', 'unknown') + if spdxid not in component_licenses: + key = f'{purl}-{spdxid}' + component_licenses[key] = { + 'purl': purl, + 'spdxid': spdxid, + 'status': status, + 'copyleft': lic['copyleft'], + 'url': lic['url'], + } + return list(component_licenses.values()) + + +# +# End of ScanResultProcessor Class +# \ No newline at end of file diff --git a/src/scanoss/osadl.py b/src/scanoss/osadl.py new file mode 100644 index 00000000..36f68b90 --- /dev/null +++ b/src/scanoss/osadl.py @@ -0,0 +1,125 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +import json +import sys + +import importlib_resources + +from scanoss.scanossbase import ScanossBase + + +class Osadl(ScanossBase): + """ + OSADL data accessor class. + + Provides access to OSADL (Open Source Automation Development Lab) authoritative + checklist data for license analysis. + + Data is loaded once at class level and shared across all instances for efficiency. + + Data source: https://www.osadl.org/fileadmin/checklists/copyleft.json + License: CC-BY-4.0 + """ + + _shared_copyleft_data = {} + _data_loaded = False + + def __init__(self, debug: bool = False, trace: bool = True, quiet: bool = False): + """ + Initialize the Osadl class. + Data is loaded once at class level and shared across all instances. + """ + super().__init__(debug, trace, quiet) + self._load_copyleft_data() + + + def _load_copyleft_data(self) -> bool: + """ + Load the embedded OSADL copyleft JSON file into class-level shared data. + Data is loaded only once and shared across all instances. + + :return: True if successful, False otherwise + """ + if Osadl._data_loaded: + return True + + # OSADL copyleft license checklist from: https://www.osadl.org/Checklists + # Data source: https://www.osadl.org/fileadmin/checklists/copyleft.json + # License: CC-BY-4.0 (Creative Commons Attribution 4.0 International) + # Copyright: (C) 2017 - 2024 Open Source Automation Development Lab (OSADL) eG + try: + f_name = importlib_resources.files(__name__) / 'data/osadl-copyleft.json' + with importlib_resources.as_file(f_name) as f: + with open(f, 'r', encoding='utf-8') as file: + data = json.load(file) + except Exception as e: + self.print_stderr(f'ERROR: Problem loading OSADL copyleft data: {e}') + return False + + # Process copyleft data + copyleft = data.get('copyleft', {}) + if not copyleft: + self.print_stderr('ERROR: No copyleft data found in OSADL JSON') + return False + + # Store in class-level shared dictionary + for lic_id, status in copyleft.items(): + # Normalize license ID (lowercase) for consistent lookup + lic_id_lc = lic_id.lower() + Osadl._shared_copyleft_data[lic_id_lc] = status + + Osadl._data_loaded = True + self.print_debug(f'Loaded {len(Osadl._shared_copyleft_data)} OSADL copyleft entries') + return True + + def is_copyleft(self, spdx_id: str) -> bool: + """ + Check if a license is copyleft according to OSADL data. + + Returns True for both strong copyleft ("Yes") and weak/restricted copyleft ("Yes (restricted)"). + + :param spdx_id: SPDX license identifier + :return: True if copyleft, False otherwise + """ + if not spdx_id: + self.print_debug('No license ID provided for copyleft check') + return False + + # Normalize lookup + spdx_id_lc = spdx_id.lower() + # Use class-level shared data + status = Osadl._shared_copyleft_data.get(spdx_id_lc) + + if not status: + self.print_debug(f'No OSADL copyleft data for license: {spdx_id}') + return False + + # Consider both "Yes" and "Yes (restricted)" as copyleft (case-insensitive) + return status.lower().startswith('yes') + + +# +# End of Osadl Class +# diff --git a/src/scanoss/results.py b/src/scanoss/results.py new file mode 100644 index 00000000..5cbff282 --- /dev/null +++ b/src/scanoss/results.py @@ -0,0 +1,275 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2024, SCANOSS + + 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. +""" + +import json +from typing import Any, Dict, List + +from scanoss.utils.abstract_presenter import AbstractPresenter + +from .scanossbase import ScanossBase + +MATCH_TYPES = ['file', 'snippet'] +STATUSES = ['pending', 'identified'] + + +AVAILABLE_FILTER_VALUES = { + 'match_type': [e for e in MATCH_TYPES], + 'status': [e for e in STATUSES], +} + + +ARG_TO_FILTER_MAP = { + 'match_type': 'id', + 'status': 'status', +} + +PENDING_IDENTIFICATION_FILTERS = { + 'match_type': ['file', 'snippet'], + 'status': ['pending'], +} + + +class ResultsPresenter(AbstractPresenter): + """ + SCANOSS Results presenter class + Handles the presentation of the scan results + """ + + def __init__(self, results_instance, **kwargs): + super().__init__(**kwargs) + self.results = results_instance + + def _format_json_output(self) -> str: + """ + Format the output data into a JSON object + """ + + formatted_data = [] + for item in self.results.data: + formatted_data.append( + { + 'file': item.get('filename'), + 'status': item.get('status', 'N/A'), + 'match_type': item['id'], + 'matched': item.get('matched', 'N/A'), + 'purl': (item.get('purl')[0] if item.get('purl') else 'N/A'), + 'license': (item.get('licenses')[0].get('name', 'N/A') if item.get('licenses') else 'N/A'), + } + ) + try: + return json.dumps({'results': formatted_data, 'total': len(formatted_data)}, indent=2) + except Exception as e: + self.base.print_stderr(f'ERROR: Problem formatting JSON output: {e}') + return '' + + def _format_cyclonedx_output(self) -> str: + raise NotImplementedError('CycloneDX output is not implemented') + + def _format_spdxlite_output(self) -> str: + raise NotImplementedError('SPDXlite output is not implemented') + + def _format_csv_output(self) -> str: + raise NotImplementedError('CSV output is not implemented') + + def _format_raw_output(self) -> str: + raise NotImplementedError('Raw output is not implemented') + + def _format_plain_output(self) -> str: + """Format the output data into a plain text string + + Returns: + str: The formatted output data + """ + if not self.results.data: + msg = 'No results to present' + return msg + + formatted = '' + for item in self.results.data: + formatted += f'{self._format_plain_output_item(item)}\n' + return formatted + + @staticmethod + def _format_plain_output_item(item): + purls = item.get('purl', []) + licenses = item.get('licenses', []) + + return ( + f'File: {item.get("filename")}\n' + f'Match type: {item.get("id")}\n' + f'Status: {item.get("status", "N/A")}\n' + f'Matched: {item.get("matched", "N/A")}\n' + f'Purl: {purls[0] if purls else "N/A"}\n' + f'License: {licenses[0].get("name", "N/A") if licenses else "N/A"}\n' + ) + + +class Results: + """ + SCANOSS Results class \n + Handles the parsing and filtering of the scan results + """ + + def __init__( # noqa: PLR0913 + self, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + filepath: str = None, + match_type: str = None, + status: str = None, + output_file: str = None, + output_format: str = None, + ): + """Initialise the Results class + + Args: + debug (bool, optional): Debug. Defaults to False. + trace (bool, optional): Trace. Defaults to False. + quiet (bool, optional): Quiet. Defaults to False. + filepath (str, optional): Path to the scan results file. Defaults to None. + match_type (str, optional): Comma separated match type filters. Defaults to None. + status (str, optional): Comma separated status filters. Defaults to None. + output_file (str, optional): Path to the output file. Defaults to None. + output_format (str, optional): Output format. Defaults to None. + """ + + self.base = ScanossBase(debug, trace, quiet) + self.data = self._load_and_transform(filepath) + self.filters = self._load_filters(match_type=match_type, status=status) + self.presenter = ResultsPresenter( + self, + debug=debug, + trace=trace, + quiet=quiet, + output_file=output_file, + output_format=output_format, + ) + + def load_file(self, file: str) -> Dict[str, Any]: + """Load the JSON file + + Args: + file (str): Path to the JSON file + + Returns: + Dict[str, Any]: The parsed JSON data + """ + with open(file, 'r') as jsonfile: + try: + return json.load(jsonfile) + except Exception as e: + self.base.print_stderr(f'ERROR: Problem parsing input JSON: {e}') + + def _load_and_transform(self, file: str) -> List[Dict[str, Any]]: + """ + Load the file and transform the data into a list of dictionaries with the filename and the file data + """ + + raw_data = self.load_file(file) + return self._transform_data(raw_data) + + @staticmethod + def _transform_data(data: dict) -> list: + """Transform the data into a list of dictionaries with the filename and the file data + + Args: + data (dict): The raw data + + Returns: + list: The transformed data + """ + result = [] + for filename, file_data in data.items(): + if file_data: + file_obj = {'filename': filename} + file_obj.update(file_data[0]) + result.append(file_obj) + return result + + def _load_filters(self, **kwargs): + """Extract and parse the filters + + Returns: + dict: Parsed filters + """ + filters = {} + + for key, value in kwargs.items(): + if value: + filters[key] = self._extract_comma_separated_values(value) + + return filters + + @staticmethod + def _extract_comma_separated_values(values: str): + return [value.strip() for value in values.split(',')] + + def apply_filters(self): + """Apply the filters to the data""" + filtered_data = [] + for item in self.data: + if self._item_matches_filters(item): + filtered_data.append(item) + self.data = filtered_data + + return self + + def _item_matches_filters(self, item): + for filter_key, filter_values in self.filters.items(): + if not filter_values: + continue + + self._validate_filter_values(filter_key, filter_values) + + item_value = item.get(ARG_TO_FILTER_MAP[filter_key]) + if isinstance(filter_values, list): + if item_value not in filter_values: + return False + elif item_value != filter_values: + return False + return True + + @staticmethod + def _validate_filter_values(filter_key: str, filter_value: List[str]): + if any(value not in AVAILABLE_FILTER_VALUES.get(filter_key, []) for value in filter_value): + valid_values = ', '.join(AVAILABLE_FILTER_VALUES.get(filter_key, [])) + raise ValueError( + f"ERROR: Invalid filter value '{filter_value}' for filter '{filter_key}'. " + f'Valid values are: {valid_values}' + ) + + def get_pending_identifications(self): + """Get files with 'pending' status and 'file' or 'snippet' match type""" + self.filters = PENDING_IDENTIFICATION_FILTERS + self.apply_filters() + + return self + + def has_results(self): + return bool(self.data) + + def present(self, output_format: str = None, output_file: str = None): + """Present the results in the selected format""" + self.presenter.present(output_format=output_format, output_file=output_file) diff --git a/src/scanoss/scan_settings_builder.py b/src/scanoss/scan_settings_builder.py new file mode 100644 index 00000000..d442ba9d --- /dev/null +++ b/src/scanoss/scan_settings_builder.py @@ -0,0 +1,311 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +from typing import TYPE_CHECKING, Optional + +from .scanossbase import ScanossBase + +if TYPE_CHECKING: + from .scanoss_settings import ScanossSettings + +MAX_RANKING_THRESHOLD = 10 + + +class ScanSettingsBuilder(ScanossBase): + """Builder class for merging CLI arguments with scanoss.json settings file values. + + This class implements an API for merging scan configuration + from multiple sources with the following priority order: + 1. settings.file_snippet section in scanoss.json (highest priority) + 2. settings section in scanoss.json (middle priority) + 3. CLI arguments (lowest priority - used as fallback) + + Attributes: + proxy: Merged proxy host URL + url: Merged API base URL + ignore_cert_errors: Whether to ignore SSL certificate errors + min_snippet_hits: Minimum snippet hits required for matching + min_snippet_lines: Minimum snippet lines required for matching + honour_file_exts: Whether to honour file extensions during scanning + ranking: Whether ranking is enabled + ranking_threshold: Ranking threshold value + """ + + def __init__( + self, + scanoss_settings: 'ScanossSettings | None', + debug: bool = False, + trace: bool = False, + quiet: bool = False, + ): + """Initialize the builder with optional scanoss settings. + + Args: + scanoss_settings: ScanossSettings instance loaded from scanoss.json, + or None if no settings file is available. + debug: Enable debug output + trace: Enable trace output + quiet: Enable quiet mode + """ + super().__init__(debug=debug, trace=trace, quiet=quiet) + self.scanoss_settings = scanoss_settings + # Merged values + self.proxy: Optional[str] = None + self.url: Optional[str] = None + self.ignore_cert_errors: bool = False + self.min_snippet_hits: Optional[int] = None + self.min_snippet_lines: Optional[int] = None + self.honour_file_exts: Optional[any] = None + self.ranking: Optional[any] = None + self.ranking_threshold: Optional[int] = None + + def with_proxy(self, cli_value: str = None) -> 'ScanSettingsBuilder': + """Set proxy host with priority: file_snippet.proxy.host > settings.proxy.host > CLI. + + Args: + cli_value: Proxy host from CLI argument (e.g., 'http://proxy:8080') + + Returns: + Self for method chaining + """ + self.proxy = self._merge_with_priority( + cli_value, + self._get_proxy_host(self._get_file_snippet_proxy()), + self._get_proxy_host(self._get_root_proxy()) + ) + return self + + def with_url(self, cli_value: str = None) -> 'ScanSettingsBuilder': + """Set API base URL with priority: file_snippet.http_config.base_uri > settings.http_config.base_uri > CLI. + + Args: + cli_value: API base URL from CLI argument (e.g., 'https://api.scanoss.com') + + Returns: + Self for method chaining + """ + self.url = self._merge_with_priority( + cli_value, + self._get_file_snippet_http_config_value('base_uri'), + self._get_http_config_value('base_uri') + ) + return self + + def with_ignore_cert_errors(self, cli_value: bool = False) -> 'ScanSettingsBuilder': + """Set ignore_cert_errors with priority: CLI True > file_snippet > settings > False. + + Note: CLI value only takes effect if True (flag present). False means + the flag was not provided, so settings file values are checked. + + Args: + cli_value: Whether to ignore SSL certificate errors from CLI flag + + Returns: + Self for method chaining + """ + result = self._merge_with_priority( + cli_value if cli_value else None, + self._get_file_snippet_http_config_value('ignore_cert_errors'), + self._get_http_config_value('ignore_cert_errors') + ) + self.ignore_cert_errors = result if result is not None else False + return self + + def with_min_snippet_hits(self, cli_value: int = None) -> 'ScanSettingsBuilder': + """Set minimum snippet hits with priority: settings.file_snippet.min_snippet_hits > CLI. + + Minimum allowed value is 0. Values below 0 will be clamped and logged. + + Args: + cli_value: Minimum snippet hits from CLI argument + + Returns: + Self for method chaining + """ + self.min_snippet_hits = self._merge_cli_with_settings( + cli_value, + self._get_file_snippet_setting('min_snippet_hits') + ) + if self.min_snippet_hits is not None and self.min_snippet_hits < 0: + self.print_msg( + f'WARNING: min-snippet-hits value {self.min_snippet_hits} is below minimum allowed (0). ' + f'Setting to 0.' + ) + self.min_snippet_hits = 0 + return self + + def with_min_snippet_lines(self, cli_value: int = None) -> 'ScanSettingsBuilder': + """Set minimum snippet lines with priority: settings.file_snippet.min_snippet_lines > CLI. + + Minimum allowed value is 0. Values below 0 will be clamped and logged. + + Args: + cli_value: Minimum snippet lines from CLI argument + + Returns: + Self for method chaining + """ + self.min_snippet_lines = self._merge_cli_with_settings( + cli_value, + self._get_file_snippet_setting('min_snippet_lines') + ) + if self.min_snippet_lines is not None and self.min_snippet_lines < 0: + self.print_msg( + f'WARNING: min-snippet-lines value {self.min_snippet_lines} is below minimum allowed (0). ' + f'Setting to 0.' + ) + self.min_snippet_lines = 0 + return self + + def with_honour_file_exts(self, cli_value: str = None) -> 'ScanSettingsBuilder': + """Set honour_file_exts with priority: settings.file_snippet.honour_file_exts > CLI. + + Args: + cli_value: String 'true', 'false', or 'unset' from CLI argument + + Returns: + Self for method chaining + """ + self.honour_file_exts = self._merge_cli_with_settings( + cli_value, + self._get_file_snippet_setting('honour_file_exts') + ) + ## Convert to boolean + if self.honour_file_exts is not None and self.honour_file_exts!= 'unset': + self.honour_file_exts = self._str_to_bool(self.honour_file_exts) + return self + + def with_ranking(self, cli_value: str = None) -> 'ScanSettingsBuilder': + """Set ranking enabled with priority: settings.file_snippet.ranking_enabled > CLI. + + Args: + cli_value: String 'true', 'false', or 'unset' from CLI argument + + Returns: + Self for method chaining + """ + self.ranking = self._merge_cli_with_settings( + cli_value, + self._get_file_snippet_setting('ranking_enabled') + ) + if self.ranking is not None and self.ranking != 'unset': + self.ranking = self._str_to_bool(self.ranking) + return self + + def with_ranking_threshold(self, cli_value: int = None) -> 'ScanSettingsBuilder': + """Set ranking threshold with priority: settings.file_snippet.ranking_threshold > CLI. + + Valid range is -1 to 10. Values outside this range will be clamped and logged. + + Args: + cli_value: Ranking threshold from CLI argument + + Returns: + Self for method chaining + """ + self.ranking_threshold = self._merge_cli_with_settings( + cli_value, + self._get_file_snippet_setting('ranking_threshold') + ) + if self.ranking_threshold is not None: + if self.ranking_threshold > MAX_RANKING_THRESHOLD: + self.print_msg( + f'WARNING: ranking-threshold value {self.ranking_threshold} exceeds maximum allowed ' + f'({MAX_RANKING_THRESHOLD}). Setting to {MAX_RANKING_THRESHOLD}.' + ) + self.ranking_threshold = MAX_RANKING_THRESHOLD + elif self.ranking_threshold < -1: + self.print_msg( + f'WARNING: ranking-threshold value {self.ranking_threshold} is below minimum allowed (-1). ' + f'Setting to -1.' + ) + self.ranking_threshold = -1 + return self + + # Private helper methods + @staticmethod + def _merge_with_priority(cli_value, file_snippet_value, root_value): + """Merge with priority: file_snippet > root settings > CLI""" + if file_snippet_value is not None: + return file_snippet_value + if root_value is not None: + return root_value + return cli_value + + @staticmethod + def _merge_cli_with_settings(cli_value, settings_value): + """Merge CLI value with settings, with settings taking priority over CLI. + + Returns settings_value if not None, otherwise falls back to cli_value. + """ + if settings_value is not None: + return settings_value + return cli_value + + + @staticmethod + def _str_to_bool(value: str) -> Optional[bool]: + """Convert string 'true'/'false' to boolean.""" + if value is None: + return None + if isinstance(value, bool): + return value + return value.lower() == 'true' + + # Methods to extract values from scanoss_settings + def _get_file_snippet_setting(self, key: str): + """Get a setting from the file_snippet section.""" + if not self.scanoss_settings: + return None + return self.scanoss_settings.get_file_snippet_settings().get(key) + + def _get_file_snippet_proxy(self): + """Get proxy config from file_snippet section.""" + return self.scanoss_settings.get_file_snippet_proxy() if self.scanoss_settings else None + + def _get_root_proxy(self): + """Get proxy config from root settings section.""" + return self.scanoss_settings.get_proxy() if self.scanoss_settings else None + + @staticmethod + def _get_proxy_host(proxy_config) -> Optional[str]: + """Extract host from proxy configuration dict.""" + if proxy_config is None: + return None + host = proxy_config.get('host') + return host if host else None + + def _get_http_config_value(self, key: str): + """Extract a value from http_config dict.""" + if not self.scanoss_settings: + return None + config = self.scanoss_settings.get_http_config() + return config.get(key) if config else None + + def _get_file_snippet_http_config_value(self, key: str): + """Extract a value from file_snippet http_config dict.""" + if not self.scanoss_settings: + return None + config = self.scanoss_settings.get_file_snippet_http_config() + return config.get(key) if config else None \ No newline at end of file diff --git a/src/scanoss/scancodedeps.py b/src/scanoss/scancodedeps.py index db08ee02..492ff2b3 100644 --- a/src/scanoss/scancodedeps.py +++ b/src/scanoss/scancodedeps.py @@ -1,40 +1,57 @@ """ - SPDX-License-Identifier: MIT - - Copyright (c) 2021, SCANOSS - - 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. +SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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. """ import json import os.path +import re import subprocess +from pathspec import GitIgnoreSpec + from .scanossbase import ScanossBase +# Regex to strip package name prefix from a requirement, keeping only the version specifier. +# e.g. 'gtest==1.17.0' -> '==1.17.0', 'boost>=1.83.0' -> '>=1.83.0', '^4.18.0' -> '^4.18.0' +REQUIREMENT_NAME_PREFIX_RE = re.compile(r'^\s*[^<>=!^]+\s*(?===|!=|>=|<=|=|>|<|\^)') + class ScancodeDeps(ScanossBase): """ SCANOSS dependency scanning class """ - def __init__(self, debug: bool = False, quiet: bool = False, trace: bool = False, output_file: str = None, - scan_output: str = None, timeout: int = 600, sc_command: str = None): + + def __init__( # noqa: PLR0913 + self, + debug: bool = False, + quiet: bool = False, + trace: bool = False, + output_file: str = None, + scan_output: str = None, + timeout: int = 600, + sc_command: str = None, + scanoss_settings=None, + ): """ Initialise ScancodeDeps class """ @@ -46,6 +63,7 @@ def __init__(self, debug: bool = False, quiet: bool = False, trace: bool = False self.scan_output = scan_output self.sc_command = sc_command if sc_command else 'scancode' self.output_file = output_file if output_file else 'scancode-dependencies.json' + self.scanoss_settings = scanoss_settings def __log_result(self, string, outfile=None): """ @@ -54,7 +72,7 @@ def __log_result(self, string, outfile=None): if not outfile and self.scan_output: outfile = self.scan_output if outfile: - with open(outfile, "a") as rf: + with open(outfile, 'a') as rf: rf.write(string + '\n') else: print(string) @@ -62,18 +80,18 @@ def __log_result(self, string, outfile=None): def remove_interim_file(self, output_file: str = None): """ Remove the temporary Scancode interim file - :param output_file: file to remove (optional) + :param output_file: filename to remove (optional) """ if not output_file and self.output_file: output_file = self.output_file if os.path.isfile(output_file): try: - self.print_trace(f'Cleaning temporary scancode files...') + self.print_trace('Cleaning temporary scancode files...') os.remove(output_file) except Exception as e: self.print_stderr(f'Warning: Failed to remove temporary file {output_file}: {e}') - def produce_from_json(self, data: json) -> dict: + def produce_from_json(self, data: json) -> dict: # noqa: PLR0912 """ Parse the given input JSON string and return Dependency summary :param data: json - JSON object @@ -82,10 +100,10 @@ def produce_from_json(self, data: json) -> dict: if not data: self.print_stderr('ERROR: No JSON data provided to parse.') return None - self.print_debug(f'Processing Scancode results into Dependency data...') + self.print_debug('Processing Scancode results into Dependency data...') files = [] for t in data: - if t == 'files': # Only interested in 'files' details + if t == 'files': # Only interested in 'files' details files_details = data.get(t) if not files_details or files_details == '': continue @@ -98,34 +116,50 @@ def produce_from_json(self, data: json) -> dict: f_type = fd.get('type') if not f_type or f_type == '' or f_type != 'file': # Only process files continue - f_packages = fd.get('packages') + f_packages = fd.get('package_data') # scancode format 2.0 if not f_packages or f_packages == '': - continue - # print(f'Path: {f_path}, Packages: {f_packages}') + f_packages = fd.get('packages') # scancode formate 1.0 + if not f_packages or f_packages == '': + continue + self.print_debug(f'Path: {f_path}, Packages: {len(f_packages)}') + purls = [] for pkgs in f_packages: pk_deps = pkgs.get('dependencies') + if not pk_deps or pk_deps == '': continue - # print(f'Path: {f_path}, Deps: {pk_deps}') - purls = [] for d in pk_deps: dp = d.get('purl') if not dp or dp == '': continue + + dp = dp.replace('"', '').replace('%22', '') # remove unwanted quotes on purls dp_data = {'purl': dp} - rq = d.get('requirement') - if rq and rq != '' and not dp.endswith(rq): - dp_data['requirement'] = rq + rq = d.get('extracted_requirement') # scancode format 2.0 + if not rq or rq == '': + rq = d.get('requirement') # scancode format 1.0 + # skip requirement if it ends with the purl (i.e. exact version) or if it's local (file) + if rq and rq != '': + # Strip any prefix data before a version comparator + rq = REQUIREMENT_NAME_PREFIX_RE.sub('', rq) + # skip if it ends with the purl (exact version) or is local (file) + if not dp.endswith(rq) and not rq.startswith('file:'): + dp_data['requirement'] = rq + + # Gets dependency scope + scope = d.get('scope') + if scope and scope != '': + dp_data['scope'] = scope + purls.append(dp_data) - # print(f'Path: {f_path}, Purls: {purls}') - if len(purls) > 0: - file = {'file': f_path, 'purls': purls} - files.append(file) + # end for loop + + if len(purls) > 0: + files.append({'file': f_path, 'purls': purls}) # End packages # End file details # End dependencies json deps = {'files': files} - # self.print_debug(f'Dep Data: {deps}') return deps def produce_from_file(self, json_file: str = None) -> json: @@ -161,7 +195,31 @@ def produce_from_str(self, json_str: str) -> dict: return None return self.produce_from_json(data) - def get_dependencies(self, output_file: str = None, what_to_scan: str = None, result_output: str = None) -> bool: + def filter_dependencies_by_path(self, deps: dict) -> dict: + """Filter dependency files by path using skip patterns from scanoss_settings. + + :param deps: dependency dict with 'files' key + :return: filtered dependency dict + """ + if not self.scanoss_settings: + return deps + patterns = self.scanoss_settings.get_skip_patterns('dependencies') + if not patterns: + return deps + spec = GitIgnoreSpec.from_lines(patterns) + all_files = deps.get('files', []) + filtered_files = [] + for f in all_files: + file_path = f.get('file', '') + if spec.match_file(file_path): + self.print_debug(f'Skipping dependency file: {file_path} (matches skip pattern)') + else: + filtered_files.append(f) + return {'files': filtered_files} + + def get_dependencies( + self, output_file: str = None, what_to_scan: str = None, result_output: str = None + ) -> bool: """ Get the dependencies for the required file/directory and output the JSON results :param output_file: temporary scanocde file to write interim results to @@ -174,6 +232,9 @@ def get_dependencies(self, output_file: str = None, what_to_scan: str = None, re return False self.print_msg('Producing summary...') deps = self.produce_from_file(output_file) + if deps: + deps = self.filter_dependencies_by_path(deps) + deps = self.__remove_dep_scope(deps) self.remove_interim_file(output_file) if not deps: return False @@ -183,7 +244,7 @@ def get_dependencies(self, output_file: str = None, what_to_scan: str = None, re def run_scan(self, output_file: str = None, what_to_scan: str = None) -> bool: """ Run a scan of the specified file/folder and output the results to temporary file - :param output_file: temporary scancode output file + :param output_file: temporary scancode output filename :param what_to_scan: file/directory to scan :return: True on success, False otherwise """ @@ -191,17 +252,33 @@ def run_scan(self, output_file: str = None, what_to_scan: str = None) -> bool: output_file = self.output_file try: open(output_file, 'w').close() - self.print_trace(f'About to execute {self.sc_command} -p --only-findings --quiet --json {output_file}' - f' {what_to_scan}') - result = subprocess.run([self.sc_command, '-p', '--only-findings', '--quiet', '--json', - output_file, what_to_scan], - cwd=os.getcwd(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - text=True, timeout=self.timeout - ) + self.print_trace( + f'About to execute {self.sc_command} -p --only-findings --quiet --json {output_file} {what_to_scan}' + ) + result = subprocess.run( + [ + self.sc_command, + '-p', + '--only-findings', + '--quiet', + '--strip-root', + '--json', + output_file, + what_to_scan, + ], + cwd=os.getcwd(), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=self.timeout, + check=False, + ) self.print_trace(f'Subprocess return: {result}') if result.returncode: - self.print_stderr(f'ERROR: Scancode dependency scan of {what_to_scan} failed with exit code' - f' {result.returncode}:\n{result.stdout}') + self.print_stderr( + f'ERROR: Scancode dependency scan of {what_to_scan} failed with exit code' + f' {result.returncode}:\n{result.stdout}' + ) return False except subprocess.TimeoutExpired as e: self.print_stderr(f'ERROR: Timed out attempting to run scancode dependency scan on {what_to_scan}: {e}') @@ -210,6 +287,42 @@ def run_scan(self, output_file: str = None, what_to_scan: str = None) -> bool: self.print_stderr(f'ERROR: Issue running scancode dependency scan on {what_to_scan}: {e}') return False return True + + def load_from_file(self, json_file: str = None) -> json: + """ + Load the parsed JSON dependencies file and return the json object + :param json_file: dependency json file + :return: SCANOSS dependency JSON + """ + if not json_file: + self.print_stderr('ERROR: No parsed JSON file provided to load.') + return None + if not os.path.isfile(json_file): + self.print_stderr(f'ERROR: parsed JSON file does not exist or is not a file: {json_file}') + return None + with open(json_file, 'r') as f: + try: + return json.loads(f.read()) + except Exception as e: + self.print_stderr(f'ERROR: Problem loading input JSON: {e}') + return None + + @staticmethod + def __remove_dep_scope(deps: json) -> json: + """ + :param deps: dependencies with scopes + :return dependencies without scopes + """ + files = deps.get('files') + for file in files: + if 'purls' in file: + purls = file.get('purls') + for purl in purls: + purl.pop('scope', None) + + return {'files': files} + + # # End of ScancodeDeps Class # diff --git a/src/scanoss/scanner.py b/src/scanoss/scanner.py index f82ae7c9..1467b328 100644 --- a/src/scanoss/scanner.py +++ b/src/scanoss/scanner.py @@ -1,193 +1,304 @@ """ - SPDX-License-Identifier: MIT - - Copyright (c) 2021, SCANOSS - - 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. +SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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. """ + +import datetime import json import os import sys +from contextlib import nullcontext +from pathlib import Path +from typing import Any, Dict, List, Optional -from progress.bar import Bar +import importlib_resources from progress.spinner import Spinner +from pypac.parser import PACFile -from .scanossapi import ScanossApi -from .winnowing import Winnowing +from scanoss.file_filters import FileFilters + +from . import __version__ +from .csvoutput import CsvOutput from .cyclonedx import CycloneDx -from .spdxlite import SpdxLite -from .threadedscanning import ThreadedScanning +from .scan_settings_builder import ScanSettingsBuilder from .scancodedeps import ScancodeDeps -from .threadeddependencies import ThreadedDependencies +from .scanoss_settings import SbomContext, ScanossSettings +from .scanossapi import ScanossApi +from .scanossbase import ScanossBase from .scanossgrpc import ScanossGrpc +from .scanpostprocessor import ScanPostProcessor from .scantype import ScanType -from .scanossbase import ScanossBase +from .spdxlite import SpdxLite +from .threadeddependencies import SCOPE, ThreadedDependencies +from .threadedscanning import ThreadedScanning + +FAST_WINNOWING = False +try: + from scanoss_winnowing.winnowing import Winnowing -FILTERED_DIRS = { # Folders to skip - "nbproject", "nbbuild", "nbdist", "__pycache__", "venv", "_yardoc", "eggs", "wheels", "htmlcov", - "__pypackages__" - } -FILTERED_DIR_EXT = { # Folder endings to skip - ".egg-info" - } -FILTERED_EXT = { # File extensions to skip - ".1", ".2", ".3", ".4", ".5", ".6", ".7", ".8", ".9", ".ac", ".adoc", ".am", - ".asciidoc", ".bmp", ".build", ".cfg", ".chm", ".class", ".cmake", ".cnf", - ".conf", ".config", ".contributors", ".copying", ".crt", ".csproj", ".css", - ".csv", ".dat", ".data", ".doc", ".docx", ".dtd", ".dts", ".iws", ".c9", ".c9revisions", - ".dtsi", ".dump", ".eot", ".eps", ".geojson", ".gdoc", ".gif", - ".glif", ".gmo", ".gradle", ".guess", ".hex", ".htm", ".html", ".ico", ".iml", - ".in", ".inc", ".info", ".ini", ".ipynb", ".jpeg", ".jpg", ".json", ".jsonld", ".lock", - ".log", ".m4", ".map", ".markdown", ".md", ".md5", ".meta", ".mk", ".mxml", - ".o", ".otf", ".out", ".pbtxt", ".pdf", ".pem", ".phtml", ".plist", ".png", - ".po", ".ppt", ".prefs", ".properties", ".pyc", ".qdoc", ".result", ".rgb", - ".rst", ".scss", ".sha", ".sha1", ".sha2", ".sha256", ".sln", ".spec", ".sql", - ".sub", ".svg", ".svn-base", ".tab", ".template", ".test", ".tex", ".tiff", - ".toml", ".ttf", ".txt", ".utf-8", ".vim", ".wav", ".whl", ".woff", ".xht", - ".xhtml", ".xls", ".xlsx", ".xml", ".xpm", ".xsd", ".xul", ".yaml", ".yml", ".wfp", - ".editorconfig", ".dotcover", ".pid", ".lcov", ".egg", ".manifest", ".cache", ".coverage", ".cover", - ".gem", ".lst", ".pickle", ".pdb", ".gml", ".pot", ".plt", - # File endings - "-doc", "changelog", "config", "copying", "license", "authors", "news", - "licenses", "notice", - "readme", "swiftdoc", "texidoc", "todo", "version", "ignore", "manifest", "sqlite", "sqlite3" - } -FILTERED_FILES = { # Files to skip - "gradlew", "gradlew.bat", "mvnw", "mvnw.cmd", "gradle-wrapper.jar", "maven-wrapper.jar", - "thumbs.db", "babel.config.js", - "license.txt", "license.md", "copying.lib", "makefile" - } -WFP_FILE_START = "file=" + FAST_WINNOWING = True +except (ModuleNotFoundError, ImportError): + FAST_WINNOWING = False + from .winnowing import Winnowing + +WFP_FILE_START = 'file=' +WFP_FILE_PARTS = 3 # WFP format: file=,, MAX_POST_SIZE = 64 * 1024 # 64k Max post size class Scanner(ScanossBase): """ SCANOSS scanning class - Hanlde the scanning of files, snippets and dependencies + Handle the scanning of files, snippets and dependencies """ - def __init__(self, wfp: str = None, scan_output: str = None, output_format: str = 'plain', - debug: bool = False, trace: bool = False, quiet: bool = False, api_key: str = None, url: str = None, - sbom_path: str = None, scan_type: str = None, flags: str = None, nb_threads: int = 5, - post_size: int = 64, timeout: int = 120, no_wfp_file: bool = False, - all_extensions: bool = False, all_folders: bool = False, hidden_files_folders: bool = False, - scan_options: int = 7, sc_timeout: int = 600, sc_command: str = None, grpc_url: str = None - ): + + def __init__( # noqa: PLR0913, PLR0915 + self, + scan_output: str = None, + output_format: str = 'plain', + debug: bool = False, + trace: bool = False, + quiet: bool = False, + api_key: str = None, + url: str = None, + flags: str = None, + nb_threads: int = 5, + post_size: int = 32, + timeout: int = 180, + all_extensions: bool = False, + all_folders: bool = False, + hidden_files_folders: bool = False, + scan_options: int = 7, + sc_timeout: int = 600, + sc_command: str = None, + grpc_url: str = None, + obfuscate: bool = False, + ignore_cert_errors: bool = False, + proxy: str = None, + grpc_proxy: str = None, + ca_cert: str = None, + pac: PACFile = None, + retry: int = 5, + hpsm: bool = False, + skip_size: int = 0, + skip_extensions=None, + skip_folders=None, + strip_hpsm_ids=None, + strip_snippet_ids=None, + skip_md5_ids=None, + scanoss_settings: 'ScanossSettings | None' = None, + req_headers: dict = None, + use_grpc: bool = False, + min_snippet_hits: int = None, + min_snippet_lines: int = None, + ranking: str = None, + ranking_threshold: int = None, + honour_file_exts: str = None, + skip_headers: bool = False, + skip_headers_limit: int = 0, + wfp_output: str = None, + ): """ - Initialise scanning class, including Winnowing, ScanossApi and ThreadedScanning + Initialise scanning class, including Winnowing, ScanossApi, ThreadedScanning """ super().__init__(debug, trace, quiet) - self.wfp = wfp if wfp else "scanner_output.wfp" + if skip_folders is None: + skip_folders = [] + if skip_extensions is None: + skip_extensions = [] self.scan_output = scan_output self.output_format = output_format - self.no_wfp_file = no_wfp_file + self.wfp_output = wfp_output self.isatty = sys.stderr.isatty() self.all_extensions = all_extensions self.all_folders = all_folders self.hidden_files_folders = hidden_files_folders self.scan_options = scan_options self._skip_snippets = True if not scan_options & ScanType.SCAN_SNIPPETS.value else False + self.hpsm = hpsm + self.skip_folders = skip_folders + self.skip_size = skip_size + self.skip_extensions = skip_extensions + self.req_headers = req_headers + self.scanoss_settings = scanoss_settings + ver_details = Scanner.version_details() + + # Get settings values for skip_headers options + file_snippet_settings = scanoss_settings.get_file_snippet_settings() if scanoss_settings else {} + settings_skip_headers = file_snippet_settings.get('skip_headers') + settings_skip_headers_limit = file_snippet_settings.get('skip_headers_limit') + + # Merge CLI values with settings (scanoss.json takes priority over CLI) + skip_headers = Scanner._merge_cli_with_settings(skip_headers, settings_skip_headers) + skip_headers_limit = Scanner._merge_cli_with_settings( + skip_headers_limit, settings_skip_headers_limit) + self.print_debug(f'Skip headers {skip_headers} with limit: {skip_headers_limit}') + + self.winnowing = Winnowing( + debug=debug, + trace=trace, + quiet=quiet, + skip_snippets=self._skip_snippets, + all_extensions=all_extensions, + obfuscate=obfuscate, + hpsm=self.hpsm, + strip_hpsm_ids=strip_hpsm_ids, + strip_snippet_ids=strip_snippet_ids, + skip_md5_ids=skip_md5_ids, + skip_headers=skip_headers, + skip_headers_limit=skip_headers_limit, + ) - self.winnowing = Winnowing(debug=debug, quiet=quiet, skip_snippets=self._skip_snippets, - all_extensions=all_extensions - ) - self.scanoss_api = ScanossApi(debug=debug, trace=trace, quiet=quiet, api_key=api_key, url=url, - sbom_path=sbom_path, scan_type=scan_type, flags=flags, timeout=timeout - ) - sc_deps = ScancodeDeps(debug=debug, quiet=quiet, trace=trace, timeout=sc_timeout, sc_command=sc_command) - grpc_api = ScanossGrpc(url=grpc_url, debug=debug, quiet=quiet, trace=trace) - self.threaded_deps = ThreadedDependencies(sc_deps, grpc_api, debug=debug, quiet=quiet, trace=trace) + # Build merged settings using builder pattern + scan_settings = (ScanSettingsBuilder(scanoss_settings, debug=debug, trace=trace, quiet=quiet) + .with_proxy(proxy) + .with_url(url) + .with_ignore_cert_errors(ignore_cert_errors) + .with_min_snippet_hits(min_snippet_hits) + .with_min_snippet_lines(min_snippet_lines) + .with_honour_file_exts(honour_file_exts) + .with_ranking(ranking) + .with_ranking_threshold(ranking_threshold)) + + self.print_debug(f'Scan settings: {scan_settings}') + + self.scanoss_api = ScanossApi( + debug=debug, + trace=trace, + quiet=quiet, + api_key=api_key, + url=scan_settings.url, + flags=flags, + timeout=timeout, + ver_details=ver_details, + ignore_cert_errors=scan_settings.ignore_cert_errors, + proxy=scan_settings.proxy, + ca_cert=ca_cert, + pac=pac, + retry=retry, + req_headers=self.req_headers, + min_snippet_hits=scan_settings.min_snippet_hits, + min_snippet_lines=scan_settings.min_snippet_lines, + honour_file_exts=scan_settings.honour_file_exts, + ranking=scan_settings.ranking, + ranking_threshold=scan_settings.ranking_threshold, + ) + sc_deps = ScancodeDeps( + debug=debug, quiet=quiet, trace=trace, timeout=sc_timeout, sc_command=sc_command, + scanoss_settings=scanoss_settings, + ) + grpc_api = ScanossGrpc( + url=grpc_url, + debug=debug, + quiet=quiet, + trace=trace, + api_key=api_key, + ver_details=ver_details, + ca_cert=ca_cert, + proxy=proxy, + pac=pac, + grpc_proxy=grpc_proxy, + req_headers=self.req_headers, + ignore_cert_errors=ignore_cert_errors, + use_grpc=use_grpc + ) + self.threaded_deps = ThreadedDependencies( + sc_deps, grpc_api, debug=debug, quiet=quiet, trace=trace + ) self.nb_threads = nb_threads if nb_threads and nb_threads > 0: - self.threaded_scan = ThreadedScanning(self.scanoss_api, debug=debug, trace=trace, quiet=quiet, - nb_threads=nb_threads - ) + self.threaded_scan = ThreadedScanning( + self.scanoss_api, debug=debug, trace=trace, quiet=quiet, nb_threads=nb_threads + ) else: self.threaded_scan = None self.max_post_size = post_size * 1024 if post_size > 0 else MAX_POST_SIZE # Set the max post size (default 64k) + self.post_file_count = post_size if post_size > 0 else 32 # Max number of files for any given POST (default 32) if self._skip_snippets: - self.max_post_size = 8 * 1024 # 8k Max post size if we're skipping snippets - - def __filter_files(self, files: list) -> list: - """ - Filter which files should be considered for processing - :param files: list of files to filter - :return list of filtered files - """ - file_list = [] - for f in files: - ignore = False - if f.startswith(".") and not self.hidden_files_folders: # Ignore all . files unless requested - ignore = True - if not ignore and not self.all_extensions: # Skip this check if we're allowing all extensions - f_lower = f.lower() - if f_lower in FILTERED_FILES: # Check for exact files to ignore - ignore = True - if not ignore: - for ending in FILTERED_EXT: # Check for file endings to ignore - if f_lower.endswith(ending): - ignore = True - break - if not ignore: - file_list.append(f) - return file_list - - def __filter_dirs(self, dirs: list) -> list: - """ - Filter which folders should be considered for processing - :param dirs: list of directories to filter - :return: list of filtered directories - """ - dir_list = [] - for d in dirs: - ignore = False - if d.startswith(".") and not self.hidden_files_folders: # Ignore all . folders unless requested - ignore = True - if not ignore and not self.all_folders: # Skip this check if we're allowing all folders - d_lower = d.lower() - if d_lower in FILTERED_DIRS: # Ignore specific folders - ignore = True - if not ignore: - for de in FILTERED_DIR_EXT: # Ignore specific folder endings - if d_lower.endswith(de): - ignore = True - break - if not ignore: - dir_list.append(d) - return dir_list + self.max_post_size = 8 * 1024 # 8k Max post size if we're skipping snippets + + self.post_processor = ( + ScanPostProcessor(scanoss_settings, debug=debug, trace=trace, quiet=quiet) if scan_settings else None + ) @staticmethod - def __strip_dir(scan_dir: str, length: int, path: str) -> str: + def _extract_file_path_from_line(line: str) -> str: """ - Strip the leading string from the specified path + Extract file path from a single WFP line. + WFP file lines have the format: file=,, + + Args: + line: Single WFP line starting with 'file=' + Returns: + File path or empty string if not found + """ + if not line.startswith(WFP_FILE_START): + return '' + parts = line[len(WFP_FILE_START):].split(',', 2) + if len(parts) >= WFP_FILE_PARTS: + return parts[2].strip() + return '' + + @staticmethod + def _iter_wfp_files(wfp_file: str): + """Yield (file_path, wfp_content) for each file entry in a WFP file. + + Parses the line-by-line WFP format into complete per-file blocks. + Parameters ---------- - scan_dir: str - Root path - length: int - length of the root path string - path: str - Path to strip + wfp_file: str + Path to the WFP file to parse + Yields + ------ + tuple[str, str]: (file_path, wfp_content) for each file entry """ - if length > 0 and path.startswith(scan_dir): - path = path[length:] - return path + current_block = '' + current_path = None + with open(wfp_file) as f: + for line in f: + if line.startswith(WFP_FILE_START): + if current_block: + yield current_path, current_block + current_path = Scanner._extract_file_path_from_line(line) + current_block = line + else: + current_block += line + if current_block: + yield current_path, current_block + + @staticmethod + def _merge_cli_with_settings(cli_value, settings_value): + """Merge CLI value with settings value (two-level priority: settings > cli). + + Args: + cli_value: Value from CLI argument + settings_value: Value from scanoss.json file_snippet settings + Returns: + Merged value with CLI taking priority over settings + """ + if settings_value is not None: + return settings_value + return cli_value @staticmethod def __count_files_in_wfp_file(wfp_file: str): @@ -207,25 +318,23 @@ def __count_files_in_wfp_file(wfp_file: str): return count @staticmethod - def valid_json_file(json_file: str) -> bool: - """ - Validate if the specified file is indeed valid JSON - :param: str JSON file to load - :return bool True if valid, False otherwise - """ - if not json_file: - self.print_stderr('ERROR: No JSON file provided to parse.') - return False - if not os.path.isfile(json_file): - self.print_stderr(f'ERROR: JSON file does not exist or is not a file: {json_file}') - return False + def version_details() -> str: + """ + Extract the date this version was produced + :return: version creation date string + """ + data = None try: - with open(json_file) as f: - data = json.load(f) + f_name = importlib_resources.files(__name__) / 'data/build_date.txt' + with importlib_resources.as_file(f_name) as f: + with open(f, 'r', encoding='utf-8') as file: + data = file.read().rstrip() except Exception as e: - Scanner.print_stderr(f'Problem parsing JSON file "{json_file}": {e}') - return False - return True + Scanner.print_stderr(f'Warning: Problem loading build time details: {e}') + if not data or len(data) == 0: + now = datetime.datetime.now() + data = f'date: {now.strftime("%Y%m%d%H%M%S")}, utime: {int(now.timestamp())}' + return f'tool: scanoss-py, version: {__version__}, {data}' def __log_result(self, string, outfile=None): """ @@ -234,7 +343,7 @@ def __log_result(self, string, outfile=None): if not outfile and self.scan_output: outfile = self.scan_output if outfile: - with open(outfile, "a") as rf: + with open(outfile, 'a') as rf: rf.write(string + '\n') else: print(string) @@ -273,36 +382,58 @@ def is_dependency_scan(self): """ if self.scan_options & ScanType.SCAN_DEPENDENCIES.value: return True - return False + file_snippet_settings = self.scanoss_settings.get_file_snippet_settings() if self.scanoss_settings else {} + return file_snippet_settings.get('dependency_analysis', False) - def scan_folder_with_options(self, scan_dir: str) -> bool: + def scan_folder_with_options( # noqa: PLR0913 + self, + scan_dir: str, + deps_file: str = None, + file_map: dict = None, + dep_scope: SCOPE = None, + dep_scope_include: str = None, + dep_scope_exclude: str = None, + ) -> bool: """ Scan the given folder for whatever scaning options that have been configured + :param dep_scope_exclude: comma separated list of dependency scopes to exclude + :param dep_scope_include: comma separated list of dependency scopes to include + :param dep_scope: Enum dependency scope to use :param scan_dir: directory to scan + :param deps_file: pre-parsed dependency file to decorate + :param file_map: mapping of obfuscated files back into originals :return: True if successful, False otherwise """ + success = True if not scan_dir: - raise Exception(f"ERROR: Please specify a folder to scan") + raise Exception('ERROR: Please specify a folder to scan') if not os.path.exists(scan_dir) or not os.path.isdir(scan_dir): - raise Exception(f"ERROR: Specified folder does not exist or is not a folder: {scan_dir}") + raise Exception(f'ERROR: Specified folder does not exist or is not a folder: {scan_dir}') if not self.is_file_or_snippet_scan() and not self.is_dependency_scan(): - raise Exception(f"ERROR: No scan options defined to scan folder: {scan_dir}") + raise Exception(f'ERROR: No scan options defined to scan folder: {scan_dir}') if self.scan_output: self.print_msg(f'Writing results to {self.scan_output}...') if self.is_dependency_scan(): - if not self.threaded_deps.run(what_to_scan=scan_dir, wait=False): # Kick off a background dependency scan + if not self.threaded_deps.run( + what_to_scan=scan_dir, + deps_file=deps_file, + wait=False, + dep_scope=dep_scope, + dep_scope_include=dep_scope_include, + dep_scope_exclude=dep_scope_exclude, + ): # Kick off a background dependency scan success = False if self.is_file_or_snippet_scan(): if not self.scan_folder(scan_dir): success = False if self.threaded_scan: - if not self.__finish_scan_threaded(): + if not self.__finish_scan_threaded(file_map): success = False return success - def scan_folder(self, scan_dir: str) -> bool: + def scan_folder(self, scan_dir: str) -> bool: # noqa: PLR0912, PLR0915 """ Scan the specified folder producing fingerprints, send to the SCANOSS API and return results @@ -312,72 +443,113 @@ def scan_folder(self, scan_dir: str) -> bool: """ success = True if not scan_dir: - raise Exception(f"ERROR: Please specify a folder to scan") + raise Exception('ERROR: Please specify a folder to scan') if not os.path.exists(scan_dir) or not os.path.isdir(scan_dir): - raise Exception(f"ERROR: Specified folder does not exist or is not a folder: {scan_dir}") + raise Exception(f'ERROR: Specified folder does not exist or is not a folder: {scan_dir}') - scan_dir_len = len(scan_dir) if scan_dir.endswith(os.path.sep) else len(scan_dir)+1 + file_filters = FileFilters( + debug=self.debug, + trace=self.trace, + quiet=self.quiet, + scanoss_settings=self.scanoss_settings, + all_extensions=self.all_extensions, + all_folders=self.all_folders, + hidden_files_folders=self.hidden_files_folders, + skip_size=self.skip_size, + skip_folders=self.skip_folders, + skip_extensions=self.skip_extensions, + operation_type='scanning', + ) self.print_msg(f'Searching {scan_dir} for files to fingerprint...') - spinner = None - if not self.quiet and self.isatty: - spinner = Spinner('Fingerprinting ') - wfp_list = [] - scan_block = '' - scan_size = 0 - queue_size = 0 - file_count = 0 - scan_started = False - for root, dirs, files in os.walk(scan_dir): - self.print_trace(f'U Root: {root}, Dirs: {dirs}, Files {files}') - dirs[:] = self.__filter_dirs(dirs) # Strip out unwanted directories - filtered_files = self.__filter_files(files) # Strip out unwanted files - self.print_debug(f'F Root: {root}, Dirs: {dirs}, Files {filtered_files}') - for file in filtered_files: # Cycle through each filtered file - path = os.path.join(root, file) - f_size = 0 - try: - f_size = os.stat(path).st_size - except: - self.print_trace(f'Ignoring missing symlink file: {file}') # Can fail if there is a broken symlink - if f_size > 0: # Ignore broken links and empty files - self.print_trace(f'Fingerprinting {path}...') - if spinner: - spinner.next() - wfp = self.winnowing.wfp_for_file(path, Scanner.__strip_dir(scan_dir, scan_dir_len, path)) + spinner_ctx = Spinner('Fingerprinting ') if (not self.quiet and self.isatty) else nullcontext() + + with spinner_ctx as spinner: + scan_block = '' + scan_size = 0 + queue_size = 0 + file_count = 0 # count all files fingerprinted + wfp_file_count = 0 # count number of files in each queue post + scan_started = False + wfp_list = [] if self.wfp_output else None # Collect WFPs if output file is specified + batch_context = None # Track SBOM context (purls, scan_type) for the current batch + + to_scan_files = file_filters.get_filtered_files_from_folder(scan_dir) + for to_scan_file in to_scan_files: + if self.threaded_scan and self.threaded_scan.stop_scanning(): + self.print_stderr('Warning: Aborting fingerprinting as the scanning service is not available.') + break + self.print_debug(f'Fingerprinting {to_scan_file}...') + if spinner: + spinner.next() + abs_path = Path(scan_dir, to_scan_file).resolve() + wfp = self.winnowing.wfp_for_file(str(abs_path), to_scan_file) + if wfp is None or wfp == '': + self.print_debug(f'No WFP returned for {to_scan_file}. Skipping.') + continue + if wfp_list is not None: wfp_list.append(wfp) - file_count += 1 - if self.threaded_scan: - wfp_size = len(wfp.encode("utf-8")) - if (wfp_size + scan_size) >= self.max_post_size: - self.threaded_scan.queue_add(scan_block) - queue_size += 1 - scan_block = '' - scan_block += wfp - scan_size = len(scan_block.encode("utf-8")) - if scan_size >= self.max_post_size: - self.threaded_scan.queue_add(scan_block) - queue_size += 1 - scan_block = '' - if queue_size > self.nb_threads and not scan_started: # Start scanning if we have something to do - scan_started = True - if not self.threaded_scan.run(wait=False): - self.print_stderr( - f'Warning: Some errors encounted while scanning. Results might be incomplete.') - success = False - # End for loop - if self.threaded_scan and scan_block: - self.threaded_scan.queue_add(scan_block) # Make sure all files have been submitted - if spinner: - spinner.finish() - - if wfp_list: - if not self.no_wfp_file or not self.threaded_scan: # Write a WFP file if no threading or not not requested - self.print_debug(f'Writing fingerprints to {self.wfp}') - with open(self.wfp, 'w') as f: + file_count += 1 + if self.threaded_scan: + wfp_size = len(wfp.encode('utf-8')) + + # Compute SBOM context for this file + file_context = ( + self.scanoss_settings.get_sbom_context(to_scan_file) + if self.scanoss_settings else SbomContext.empty() + ) + + # TODO: extract batching/flush logic into a shared helper to deduplicate + # scan_folder, scan_files, and scan_wfp_file_threaded + # FLUSH: Context changed (different purls or scan_type) + if scan_block != '' and batch_context is not None and file_context != batch_context: + sbom = batch_context.to_payload() if batch_context else None + self.threaded_scan.queue_add(scan_block, sbom=sbom) + queue_size += 1 + scan_block = '' + wfp_file_count = 0 + batch_context = None + + # FLUSH: Current file won't fit in batch (size limit) + if scan_block != '' and (wfp_size + scan_size) >= self.max_post_size: + sbom = batch_context.to_payload() if batch_context else None + self.threaded_scan.queue_add(scan_block, sbom=sbom) + queue_size += 1 + scan_block = '' + wfp_file_count = 0 + batch_context = None + + # ADD current file to batch + scan_block += wfp + batch_context = file_context + scan_size = len(scan_block.encode('utf-8')) + wfp_file_count += 1 + + # FLUSH: Batch is full (file count or size limit) + if wfp_file_count > self.post_file_count or scan_size >= self.max_post_size: + sbom = batch_context.to_payload() if batch_context else None + self.threaded_scan.queue_add(scan_block, sbom=sbom) + queue_size += 1 + scan_block = '' + wfp_file_count = 0 + batch_context = None + if not scan_started and queue_size > self.nb_threads: # Start scanning if we have something to do + scan_started = True + if not self.threaded_scan.run(wait=False): + self.print_stderr( + 'Warning: Some errors encountered while scanning. ' + 'Results might be incomplete.' + ) + success = False + # End for loop + if self.threaded_scan and scan_block != '': + sbom = batch_context.to_payload() if batch_context else None + self.threaded_scan.queue_add(scan_block, sbom=sbom) # Make sure all files have been submitted + + if file_count > 0: + if wfp_list is not None: + self.print_debug(f'Writing fingerprints to {self.wfp_output}') + with open(self.wfp_output, 'w') as f: f.write(''.join(wfp_list)) - else: - self.print_debug( f'Skipping writing WFP file {self.wfp}') - wfp_list = None if self.threaded_scan: success = self.__run_scan_threaded(scan_started, file_count) else: @@ -386,7 +558,7 @@ def scan_folder(self, scan_dir: str) -> bool: def __run_scan_threaded(self, scan_started: bool, file_count: int) -> bool: """ - Finish scanning the filtered files and but do not wait for it to complete + Start scanning the filtered files but do not wait for it to complete :param scan_started: If the scan has already started or not :param file_count: Number of total files to be scanned :return: True if successful, False otherwise @@ -394,119 +566,146 @@ def __run_scan_threaded(self, scan_started: bool, file_count: int) -> bool: success = True self.threaded_scan.update_bar(create=True, file_count=file_count) if not scan_started: - if not self.threaded_scan.run(wait=False): # Run the scan but do not wait for it to complete - self.print_stderr(f'Warning: Some errors encounted while scanning. Results might be incomplete.') + if not self.threaded_scan.run(wait=False): # Run the scan but do not wait for it to complete + self.print_stderr('Warning: Some errors encounted while scanning. Results might be incomplete.') success = False return success - def __finish_scan_threaded(self) -> bool: - """ - Wait for the threaded scans to complete - :return: True if successful, False otherwise + def __finish_scan_threaded(self, file_map: Optional[Dict[Any, Any]] = None) -> bool: + """Wait for the threaded scan to complete and process the results + + Args: + file_map: Mapping of obfuscated files back to originals + + Returns: + bool: True if successful, False otherwise + + Raises: + ValueError: If output format is invalid """ - success = True - responses = None + success: bool = True + scan_responses = None dep_responses = None if self.is_file_or_snippet_scan(): - if not self.threaded_scan.complete(): # Wait for the scans to complete - self.print_stderr(f'Warning: Scanning analysis ran into some trouble.') + if not self.threaded_scan.complete(): # Wait for the scans to complete + self.print_stderr('Warning: Scanning analysis ran into some trouble.') success = False self.threaded_scan.complete_bar() - responses = self.threaded_scan.responses + scan_responses = self.threaded_scan.responses if self.is_dependency_scan(): self.print_msg('Retrieving dependency data...') if not self.threaded_deps.complete(): - self.print_stderr(f'Warning: Dependency analysis ran into some trouble.') + self.print_stderr('Warning: Dependency analysis ran into some trouble.') success = False dep_responses = self.threaded_deps.responses - # self.print_stderr(f'Dep Data: {dep_responses}') - # TODO change to dictionary - raw_output = "{\n" - # TODO look into merging the two dictionaries. See https://favtutor.com/blogs/merge-dictionaries-python - if responses or dep_responses: - first = True - if responses: - for scan_resp in responses: - if scan_resp is not None: - for key, value in scan_resp.items(): - if first: - raw_output += " \"%s\":%s" % (key, json.dumps(value, indent=2)) - first = False - else: - raw_output += ",\n \"%s\":%s" % (key, json.dumps(value, indent=2)) - # End for loop - if dep_responses: - dep_files = dep_responses.get("files") - if dep_files and len(dep_files) > 0: - for dep_file in dep_files: - file = dep_file.pop("file", None) - if file is not None: - if first: - raw_output += " \"%s\":[%s]" % (file, json.dumps(dep_file, indent=2)) - first = False - else: - raw_output += ",\n \"%s\":[%s]" % (file, json.dumps(dep_file, indent=2)) - # End for loop + # If anything fails during scanning or decoration, then do not produce a results file + if not success: + self.print_stderr('Error: Scanning analysis ran into some trouble. Not producing results file.') + return success + raw_scan_results = self._merge_scan_results(scan_responses, dep_responses, file_map) + + if self.post_processor: + results = self.post_processor.load_results(raw_scan_results).post_process() else: - success = False - raw_output += "\n}" - parsed_json = None - try: - parsed_json = json.loads(raw_output) - except Exception as e: - self.print_stderr(f'Warning: Problem decoding parsed json: {e}') + results = raw_scan_results if self.output_format == 'plain': - if parsed_json: - self.__log_result(json.dumps(parsed_json, indent=2, sort_keys=True)) - else: - self.__log_result(raw_output) + self.__log_result(json.dumps(results, indent=2, sort_keys=True)) elif self.output_format == 'cyclonedx': cdx = CycloneDx(self.debug, self.scan_output) - if parsed_json: - success = cdx.produce_from_json(parsed_json) - else: - success = cdx.produce_from_str(raw_output) + success, _ = cdx.produce_from_json(results) elif self.output_format == 'spdxlite': spdxlite = SpdxLite(self.debug, self.scan_output) - if parsed_json: - success = spdxlite.produce_from_json(parsed_json) - else: - success = spdxlite.produce_from_str(raw_output) + success = spdxlite.produce_from_json(results) + elif self.output_format == 'csv': + csvo = CsvOutput(self.debug, self.scan_output) + success = csvo.produce_from_json(results) else: self.print_stderr(f'ERROR: Unknown output format: {self.output_format}') success = False return success + def _merge_scan_results( + self, + scan_responses: Optional[List], + dep_responses: Optional[Dict[str, Any]], + file_map: Optional[Dict[str, Any]], + ) -> Dict[str, Any]: + """Merge scan and dependency responses into a single dictionary""" + results: Dict[str, Any] = {} + + if scan_responses: + for response in scan_responses: + if response is not None: + if file_map: + response = self._deobfuscate_filenames(response, file_map) # noqa: PLW2901 + results.update(response) + + dep_files = dep_responses.get('files', None) if dep_responses else None + if dep_files: + for dep_file in dep_files: + file = dep_file.pop('file', None) + if file: + results[file] = [dep_file] + + return results - def scan_file_with_options(self, file: str) -> bool: + def _deobfuscate_filenames(self, response: dict, file_map: dict) -> dict: + """Convert obfuscated filenames back to original names""" + deobfuscated = {} + for key, value in response.items(): + deobfuscated_name = file_map.get(key, None) + if deobfuscated_name: + deobfuscated[deobfuscated_name] = value + else: + deobfuscated[key] = value + return deobfuscated + + def scan_file_with_options( # noqa: PLR0913 + self, + file: str, + deps_file: str = None, + file_map: dict = None, + dep_scope: SCOPE = None, + dep_scope_include: str = None, + dep_scope_exclude: str = None, + ) -> bool: """ Scan the given file for whatever scaning options that have been configured + :param dep_scope: :param file: file to scan + :param deps_file: pre-parsed dependency file to decorate + :param file_map: mapping of obfuscated files back into originals :return: True if successful, False otherwise """ success = True if not file: - raise Exception(f"ERROR: Please specify a file to scan") + raise Exception('ERROR: Please specify a file to scan') if not os.path.exists(file) or not os.path.isfile(file): - raise Exception(f"ERROR: Specified file does not exist or is not a file: {file}") + raise Exception(f'ERROR: Specified file does not exist or is not a file: {file}') if not self.is_file_or_snippet_scan() and not self.is_dependency_scan(): - raise Exception(f"ERROR: No scan options defined to scan file: {file}") + raise Exception(f'ERROR: No scan options defined to scan file: {file}') if self.scan_output: self.print_msg(f'Writing results to {self.scan_output}...') if self.is_dependency_scan(): - if not self.threaded_deps.run(what_to_scan=file, wait=False): # Kick off a background dependency scan + if not self.threaded_deps.run( + what_to_scan=file, + deps_file=deps_file, + wait=False, + dep_scope=dep_scope, + dep_scope_include=dep_scope_include, + dep_scope_exclude=dep_scope_exclude, + ): # Kick off a background dependency scan success = False if self.is_file_or_snippet_scan(): if not self.scan_file(file): success = False if self.threaded_scan: - if not self.__finish_scan_threaded(): + if not self.__finish_scan_threaded(file_map): success = False return success - def scan_file(self, file: str) -> bool: """ Scan the specified file and produce a result @@ -518,14 +717,19 @@ def scan_file(self, file: str) -> bool: """ success = True if not file: - raise Exception(f"ERROR: Please specify a file to scan") + raise Exception('ERROR: Please specify a file to scan') if not os.path.exists(file) or not os.path.isfile(file): - raise Exception(f"ERROR: Specified files does not exist or is not a file: {file}") + raise Exception(f'ERROR: Specified files does not exist or is not a file: {file}') self.print_debug(f'Fingerprinting {file}...') wfp = self.winnowing.wfp_for_file(file, file) - if wfp: + if wfp is not None and wfp != '': if self.threaded_scan: - self.threaded_scan.queue_add(wfp) # Submit the WFP for scanning + file_context = ( + self.scanoss_settings.get_sbom_context(file) + if self.scanoss_settings else SbomContext.empty() + ) + sbom = file_context.to_payload() + self.threaded_scan.queue_add(wfp, sbom=sbom) # Submit the WFP for scanning self.print_debug(f'Scanning {file}...') if self.threaded_scan: success = self.__run_scan_threaded(False, 1) @@ -533,166 +737,300 @@ def scan_file(self, file: str) -> bool: success = False return success - def scan_wfp_file(self, file: str = None) -> bool: + def scan_files(self, files: []) -> bool: # noqa: PLR0912, PLR0915 """ - Scan the contents of the specified WFP file (in the current process) - Parameters - ---------- - file: str - WFP file to scan (optional) - return: True if successful, False otherwise + Scan the specified list of files, producing fingerprints, send to the SCANOSS API and return results + Please note that by providing an explicit list you bypass any exclusions that may be defined on the scanner + :param files: list[str] + List of filenames to scan + :return True if successful, False otherwise """ success = True - wfp_file = file if file else self.wfp # If a WFP file is specified, use it, otherwise us the default - if not os.path.exists(wfp_file) or not os.path.isfile(wfp_file): - raise Exception(f"ERROR: Specified WFP file does not exist or is not a file: {wfp_file}") - file_count = Scanner.__count_files_in_wfp_file(wfp_file) - cur_files = 0 - cur_size = 0 - batch_files = 0 - wfp = '' - max_component = {'name': '', 'hits': 0} - components = {} - self.print_debug(f'Found {file_count} files to process.') - raw_output = "{\n" - file_print = '' - bar = None - if not self.quiet and self.isatty: - bar = Bar('Scanning', max=file_count) - bar.next(0) - with open(wfp_file) as f: - for line in f: - if line.startswith(WFP_FILE_START): - if file_print: - wfp += file_print # Store the WFP for the current file - cur_size = len(wfp.encode("utf-8")) - file_print = line # Start storing the next file - cur_files += 1 - batch_files += 1 - else: - file_print += line # Store the rest of the WFP for this file - l_size = cur_size + len(file_print.encode('utf-8')) - # Hit the max post size, so sending the current batch and continue processing - if l_size >= self.max_post_size and wfp: - self.print_debug(f'Sending {batch_files} ({cur_files}) of' - f' {file_count} ({len(wfp.encode("utf-8"))} bytes) files to the ScanOSS API.') - if cur_size > self.max_post_size: - Scanner.print_stderr(f'Warning: Post size {cur_size} greater than limit {self.max_post_size}') - scan_resp = self.scanoss_api.scan(wfp, max_component['name']) # Scan current WFP and store - if bar: - bar.next(batch_files) - if scan_resp is not None: - for key, value in scan_resp.items(): - raw_output += " \"%s\":%s," % (key, json.dumps(value, indent=2)) - for v in value: - if hasattr(v, 'get'): - if v.get('id') != 'none': - vcv = '%s:%s:%s' % (v.get('vendor'), v.get('component'), v.get('version')) - components[vcv] = components[vcv] + 1 if vcv in components else 1 - if max_component['hits'] < components[vcv]: - max_component['name'] = v.get('component') - max_component['hits'] = components[vcv] - else: - Scanner.print_stderr(f'Warning: Unknown value: {v}') - else: - success = False - batch_files = 0 - wfp = '' - if file_print: - wfp += file_print # Store the WFP for the current file - if wfp: - self.print_debug(f'Sending {batch_files} ({cur_files}) of' - f' {file_count} ({len(wfp.encode("utf-8"))} bytes) files to the ScanOSS API.') - scan_resp = self.scanoss_api.scan(wfp, max_component['name']) # Scan current WFP and store - if bar: - bar.next(batch_files) - first = True - if scan_resp is not None: - for key, value in scan_resp.items(): - if first: - raw_output += " \"%s\":%s" % (key, json.dumps(value, indent=2)) - first = False - else: - raw_output += ",\n \"%s\":%s" % (key, json.dumps(value, indent=2)) - else: + if not files: + raise Exception('ERROR: Please provide a non-empty list of filenames to scan') + + file_filters = FileFilters( + debug=self.debug, + trace=self.trace, + quiet=self.quiet, + scanoss_settings=self.scanoss_settings, + all_extensions=self.all_extensions, + all_folders=self.all_folders, + hidden_files_folders=self.hidden_files_folders, + skip_size=self.skip_size, + skip_folders=self.skip_folders, + skip_extensions=self.skip_extensions, + operation_type='scanning', + ) + spinner_ctx = Spinner('Fingerprinting ') if (not self.quiet and self.isatty) else nullcontext() + + with spinner_ctx as spinner: + scan_block = '' + scan_size = 0 + queue_size = 0 + file_count = 0 # count all files fingerprinted + wfp_file_count = 0 # count number of files in each queue post + scan_started = False + wfp_list = [] if self.wfp_output else None # Collect WFPs if output file is specified + batch_context = None # Track SBOM context (purls, scan_type) for the current batch + + to_scan_files = file_filters.get_filtered_files_from_files(files) + for file in to_scan_files: + if self.threaded_scan and self.threaded_scan.stop_scanning(): + self.print_stderr('Warning: Aborting fingerprinting as the scanning service is not available.') + break + self.print_debug(f'Fingerprinting {file}...') + if spinner: + spinner.next() + wfp = self.winnowing.wfp_for_file(file, file) + if wfp is None or wfp == '': + self.print_debug(f'No WFP returned for {file}. Skipping.') + continue + if wfp_list is not None: + wfp_list.append(wfp) + file_count += 1 + if self.threaded_scan: + wfp_size = len(wfp.encode('utf-8')) + + # Compute SBOM context for this file + file_context = ( + self.scanoss_settings.get_sbom_context(file) + if self.scanoss_settings else SbomContext.empty() + ) + + # FLUSH: Context changed (different purls or scan_type) + if batch_context is not None and file_context != batch_context and scan_block != '': + sbom = batch_context.to_payload() if batch_context else None + self.threaded_scan.queue_add(scan_block, sbom=sbom) + queue_size += 1 + scan_block = '' + wfp_file_count = 0 + batch_context = None + + # FLUSH: Current file won't fit in batch (size limit) + if scan_block != '' and (wfp_size + scan_size) >= self.max_post_size: + sbom = batch_context.to_payload() if batch_context else None + self.threaded_scan.queue_add(scan_block, sbom=sbom) + queue_size += 1 + scan_block = '' + wfp_file_count = 0 + batch_context = None + + # ADD current file to batch + scan_block += wfp + batch_context = file_context + scan_size = len(scan_block.encode('utf-8')) + wfp_file_count += 1 + + # FLUSH: Batch is full (file count or size limit) + if wfp_file_count > self.post_file_count or scan_size >= self.max_post_size: + sbom = batch_context.to_payload() if batch_context else None + self.threaded_scan.queue_add(scan_block, sbom=sbom) + queue_size += 1 + scan_block = '' + wfp_file_count = 0 + batch_context = None + if not scan_started and queue_size > self.nb_threads: # Start scanning if we have something to do + scan_started = True + if not self.threaded_scan.run(wait=False): + self.print_stderr( + 'Warning: Some errors encountered while scanning. ' + 'Results might be incomplete.' + ) + success = False + + # End for loop + if self.threaded_scan and scan_block != '': + sbom = batch_context.to_payload() if batch_context else None + self.threaded_scan.queue_add(scan_block, sbom=sbom) # Make sure all files have been submitted + + if file_count > 0: + if wfp_list is not None: + self.print_debug(f'Writing fingerprints to {self.wfp_output}') + with open(self.wfp_output, 'w') as f: + f.write(''.join(wfp_list)) + if self.threaded_scan: + success = self.__run_scan_threaded(scan_started, file_count) + else: + Scanner.print_stderr(f'Warning: No files found to scan from: {to_scan_files}') + return success + + def scan_files_with_options(self, files: [], deps_file: str = None, file_map: dict = None) -> bool: + """ + Scan the given list of files for whatever scaning options that have been configured + :param files: list of files to scan + :param deps_file: pre-parsed dependency file to decorate + :param file_map: mapping of obfuscated files back into originals + :return: True if successful, False otherwise + """ + success = True + if not files: + raise Exception('ERROR: Please specify a list of files to scan') + if not self.is_file_or_snippet_scan(): + raise Exception(f'ERROR: file or snippet scan options have to be set to scan files: {files}') + if self.is_dependency_scan() or deps_file: + raise Exception( + 'ERROR: The dependency scan option is currently not supported when scanning a list of files' + ) + if self.scan_output: + self.print_msg(f'Writing results to {self.scan_output}...') + if self.is_file_or_snippet_scan(): + if not self.scan_files(files): success = False - raw_output += "\n}" - if bar: - bar.finish() - if self.output_format == 'plain': - self.__log_result(raw_output) - elif self.output_format == 'cyclonedx': - cdx = CycloneDx(self.debug, self.scan_output) - cdx.produce_from_str(raw_output) - elif self.output_format == 'spdxlite': - spdxlite = SpdxLite(self.debug, self.scan_output) - success = spdxlite.produce_from_str(raw_output) + if self.threaded_scan: + if not self.__finish_scan_threaded(file_map): + success = False + return success + + def scan_contents(self, filename: str, contents: bytes) -> bool: + """ + Scan the given contents as a file + + :param filename: filename to associate with the contents + :param contents: file contents + :return: True if successful, False otherwise + """ + success = True + if not filename: + raise Exception('ERROR: Please specify a filename to scan') + if not contents: + raise Exception('ERROR: Please specify a file contents to scan') + + self.print_debug(f'Fingerprinting {filename}...') + wfp = self.winnowing.wfp_for_contents(filename, False, contents) + if wfp is not None and wfp != '': + if self.threaded_scan: + file_context = ( + self.scanoss_settings.get_sbom_context(filename) + if self.scanoss_settings else SbomContext.empty() + ) + sbom = file_context.to_payload() + self.threaded_scan.queue_add(wfp, sbom=sbom) # Submit the WFP for scanning + self.print_debug(f'Scanning {filename}...') + if self.threaded_scan: + success = self.__run_scan_threaded(False, 1) else: - self.print_stderr(f'ERROR: Unknown output format: {self.output_format}') success = False + if self.threaded_scan: + if not self.__finish_scan_threaded(): + success = False + return success + def scan_wfp_with_options(self, wfp_file: str, deps_file: str, file_map: dict = None) -> bool: + """ + Scan the given WFP file for whatever scaning options that have been configured + :param wfp_file: WFP file to scan + :param deps_file: pre-parsed dependency file to decorate + :param file_map: mapping of obfuscated files back into originals + :return: True if successful, False otherwise + """ + success = True + if not wfp_file: + raise Exception('ERROR: Please specify a WFP file to scan') + if not os.path.exists(wfp_file) or not os.path.isfile(wfp_file): + raise Exception(f'ERROR: Specified WFP file does not exist or is not a file: {wfp_file}') + + if not self.is_file_or_snippet_scan() and not self.is_dependency_scan(): + raise Exception(f'ERROR: No scan options defined to scan WFP: {wfp_file}') + + if self.scan_output: + self.print_msg(f'Writing results to {self.scan_output}...') + if self.is_dependency_scan(): + if not self.threaded_deps.run(deps_file=deps_file, wait=False): # Kick off a background dependency scan + success = False + if self.is_file_or_snippet_scan(): + if not self.scan_wfp_file_threaded(wfp_file): + success = False + if self.threaded_scan: + if not self.__finish_scan_threaded(file_map): + success = False return success - def scan_wfp_file_threaded(self, file: str = None) -> bool: + def scan_wfp_file_threaded(self, wfp_file: str) -> bool: # noqa: PLR0915 """ Scan the contents of the specified WFP file (threaded) - Parameters - ---------- - file: str - WFP file to scan (optional) + :param wfp_file: WFP file to scan return: True if successful, False otherwise """ success = True - wfp_file = file if file else self.wfp # If a WFP file is specified, use it, otherwise us the default + if not wfp_file: + raise Exception('ERROR: Please specify a WFP file to scan') if not os.path.exists(wfp_file) or not os.path.isfile(wfp_file): - raise Exception(f"ERROR: Specified WFP file does not exist or is not a file: {wfp_file}") - cur_size = 0 - scan_size = 0 + raise Exception(f'ERROR: Specified WFP file does not exist or is not a file: {wfp_file}') queue_size = 0 - file_count = 0 + file_count = 0 # count all files fingerprinted + wfp_file_count = 0 # count number of files in each queue post scan_started = False - wfp = '' scan_block = '' - with open(wfp_file) as f: # Parse the WFP file - for line in f: - if line.startswith(WFP_FILE_START): - if scan_block: - wfp += scan_block # Store the WFP for the current file - cur_size = len(wfp.encode("utf-8")) - scan_block = line # Start storing the next file - file_count += 1 - else: - scan_block += line # Store the rest of the WFP for this file - l_size = cur_size + len(scan_block.encode('utf-8')) - # Hit the max post size, so sending the current batch and continue processing - if l_size >= self.max_post_size and wfp: - if cur_size > self.max_post_size: - Scanner.print_stderr(f'Warning: Post size {cur_size} greater than limit {self.max_post_size}') - self.threaded_scan.queue_add(wfp) - queue_size += 1 - wfp = '' - if queue_size > self.nb_threads and not scan_started: # Start scanning if we have something to do - scan_started = True - if not self.threaded_scan.run(wait=False): - self.print_stderr( - f'Warning: Some errors encounted while scanning. Results might be incomplete.') - success = False - # End for loop + scan_size = 0 + batch_context = None # Track SBOM context for the current batch + + for file_path, wfp in self._iter_wfp_files(wfp_file): + file_count += 1 + wfp_size = len(wfp.encode('utf-8')) + + file_context = ( + self.scanoss_settings.get_sbom_context(file_path) + if self.scanoss_settings else SbomContext.empty() + ) + + # FLUSH: Context changed (different purls or scan_type) + if scan_block != '' and batch_context is not None and file_context != batch_context: + sbom = batch_context.to_payload() if batch_context else None + self.threaded_scan.queue_add(scan_block, sbom=sbom) + queue_size += 1 + scan_block = '' + wfp_file_count = 0 + batch_context = None + + # FLUSH: Current file won't fit in batch (size limit) + if scan_block != '' and (wfp_size + scan_size) >= self.max_post_size: + sbom = batch_context.to_payload() if batch_context else None + self.threaded_scan.queue_add(scan_block, sbom=sbom) + queue_size += 1 + scan_block = '' + wfp_file_count = 0 + batch_context = None + + # ADD current file to batch + scan_block += wfp + batch_context = file_context + scan_size = len(scan_block.encode('utf-8')) + wfp_file_count += 1 + + # FLUSH: Batch is full (file count or size limit) + if wfp_file_count > self.post_file_count or scan_size >= self.max_post_size: + sbom = batch_context.to_payload() if batch_context else None + self.threaded_scan.queue_add(scan_block, sbom=sbom) + queue_size += 1 + scan_block = '' + wfp_file_count = 0 + batch_context = None + + if not scan_started and queue_size > self.nb_threads: + scan_started = True + if not self.threaded_scan.run(wait=False): + self.print_stderr( + 'Warning: Some errors encountered while scanning. Results might be incomplete.' + ) + success = False + # End for loop if scan_block: - wfp += scan_block # Store the WFP for the current file - if wfp: - self.threaded_scan.queue_add(wfp) + sbom = batch_context.to_payload() if batch_context else None + self.threaded_scan.queue_add(scan_block, sbom=sbom) queue_size += 1 if not self.__run_scan_threaded(scan_started, file_count): success = False - elif not self.__finish_scan_threaded(): - success = False return success def scan_wfp(self, wfp: str) -> bool: """ - Send the specified (single) WFP to ScanOSS for identification + Send the specified (single) WFP to ScanOSS for identification. + + NOTE: This method does not support the scanoss settings file (scanoss.json). + For folder-level BOM filtering, use scan_wfp_with_options instead. + Parameters ---------- wfp: str @@ -700,15 +1038,15 @@ def scan_wfp(self, wfp: str) -> bool: """ success = True if not wfp: - raise Exception(f"ERROR: Please specify a WFP to scan") - raw_output = "{\n" + raise Exception('ERROR: Please specify a WFP to scan') + raw_output = '{\n' scan_resp = self.scanoss_api.scan(wfp) if scan_resp is not None: for key, value in scan_resp.items(): - raw_output += " \"%s\":%s" % (key, json.dumps(value, indent=2)) + raw_output += ' "%s":%s' % (key, json.dumps(value, indent=2)) else: success = False - raw_output += "\n}" + raw_output += '\n}' if self.output_format == 'plain': self.__log_result(raw_output) elif self.output_format == 'cyclonedx': @@ -717,20 +1055,49 @@ def scan_wfp(self, wfp: str) -> bool: elif self.output_format == 'spdxlite': spdxlite = SpdxLite(self.debug, self.scan_output) success = spdxlite.produce_from_str(raw_output) + elif self.output_format == 'csv': + csvo = CsvOutput(self.debug, self.scan_output) + csvo.produce_from_str(raw_output) else: self.print_stderr(f'ERROR: Unknown output format: {self.output_format}') success = False return success + def wfp_contents(self, filename: str, contents: bytes, wfp_file: str = None): + """ + Fingerprint the specified contents as a file + + :param filename: filename to associate with the contents + :param contents: file contents + :param wfp_file: WFP to write results to (optional) + :return: + """ + if not filename: + raise Exception('ERROR: Please specify a filename to scan') + if not contents: + raise Exception('ERROR: Please specify a file contents to scan') + + self.print_debug(f'Fingerprinting {filename}...') + wfp = self.winnowing.wfp_for_contents(filename, False, contents) + if wfp: + if wfp_file: + self.print_stderr(f'Writing fingerprints to {wfp_file}') + with open(wfp_file, 'w') as f: + f.write(wfp) + else: + print(wfp) + else: + Scanner.print_stderr(f'Warning: No fingerprints generated for: {wfp_file}') + def wfp_file(self, scan_file: str, wfp_file: str = None): """ Fingerprint the specified file """ if not scan_file: - raise Exception(f"ERROR: Please specify a file to fingerprint") + raise Exception('ERROR: Please specify a file to fingerprint') if not os.path.exists(scan_file) or not os.path.isfile(scan_file): - raise Exception(f"ERROR: Specified file does not exist or is not a file: {scan_file}") + raise Exception(f'ERROR: Specified file does not exist or is not a file: {scan_file}') self.print_debug(f'Fingerprinting {scan_file}...') wfp = self.winnowing.wfp_for_file(scan_file, scan_file) @@ -749,22 +1116,34 @@ def wfp_folder(self, scan_dir: str, wfp_file: str = None): Fingerprint the specified folder producing fingerprints """ if not scan_dir: - raise Exception(f"ERROR: Please specify a folder to fingerprint") + raise Exception('ERROR: Please specify a folder to fingerprint') if not os.path.exists(scan_dir) or not os.path.isdir(scan_dir): - raise Exception(f"ERROR: Specified folder does not exist or is not a folder: {scan_dir}") + raise Exception(f'ERROR: Specified folder does not exist or is not a folder: {scan_dir}') + file_filters = FileFilters( + debug=self.debug, + trace=self.trace, + quiet=self.quiet, + scanoss_settings=self.scanoss_settings, + all_extensions=self.all_extensions, + all_folders=self.all_folders, + hidden_files_folders=self.hidden_files_folders, + skip_size=self.skip_size, + skip_folders=self.skip_folders, + skip_extensions=self.skip_extensions, + operation_type='scanning', + ) wfps = '' - scan_dir_len = len(scan_dir) if scan_dir.endswith(os.path.sep) else len(scan_dir)+1 self.print_msg(f'Searching {scan_dir} for files to fingerprint...') - for root, dirs, files in os.walk(scan_dir): - dirs[:] = self.__filter_dirs(dirs) # Strip out unwanted directories - filtered_files = self.__filter_files(files) # Strip out unwanted files - self.print_trace(f'Root: {root}, Dirs: {dirs}, Files {filtered_files}') - for file in filtered_files: - path = os.path.join(root, file) - file_stat = os.stat(path) - if file_stat.st_size > 0: # Ignore empty files - self.print_debug(f'Fingerprinting {path}...') - wfps += self.winnowing.wfp_for_file(path, Scanner.__strip_dir(scan_dir, scan_dir_len, path)) + spinner_ctx = Spinner('Fingerprinting ') if (not self.quiet and self.isatty) else nullcontext() + + with spinner_ctx as spinner: + to_fingerprint_files = file_filters.get_filtered_files_from_folder(scan_dir) + for file in to_fingerprint_files: + if spinner: + spinner.next() + abs_path = Path(scan_dir, file).resolve() + self.print_debug(f'Fingerprinting {file}...') + wfps += self.winnowing.wfp_for_file(str(abs_path), file) if wfps: if wfp_file: self.print_stderr(f'Writing fingerprints to {wfp_file}') @@ -775,6 +1154,7 @@ def wfp_folder(self, scan_dir: str, wfp_file: str = None): else: Scanner.print_stderr(f'Warning: No files found to fingerprint in folder: {scan_dir}') + # # End of ScanOSS Class # diff --git a/src/scanoss/scanners/__init__.py b/src/scanoss/scanners/__init__.py new file mode 100644 index 00000000..1e95c46d --- /dev/null +++ b/src/scanoss/scanners/__init__.py @@ -0,0 +1,23 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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/src/scanoss/scanners/container_scanner.py b/src/scanoss/scanners/container_scanner.py new file mode 100644 index 00000000..913d8804 --- /dev/null +++ b/src/scanoss/scanners/container_scanner.py @@ -0,0 +1,476 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2024, SCANOSS + + 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. +""" + +import json +import os +import subprocess +from dataclasses import dataclass +from typing import Dict, List, Optional, TypedDict + +from scanoss.constants import DEFAULT_RETRY, DEFAULT_TIMEOUT +from scanoss.csvoutput import CsvOutput +from scanoss.cyclonedx import CycloneDx +from scanoss.scanossbase import ScanossBase +from scanoss.scanossgrpc import ScanossGrpc +from scanoss.spdxlite import SpdxLite +from scanoss.utils.abstract_presenter import AbstractPresenter + +DEFAULT_SYFT_TIMEOUT = 600 +DEFAULT_SYFT_COMMAND = 'syft' + + +@dataclass +class ContainerScannerConfig: + debug: bool = False + trace: bool = False + quiet: bool = False + retry: int = DEFAULT_RETRY + timeout: int = DEFAULT_TIMEOUT + output: Optional[str] = None + format: Optional[str] = None + apiurl: Optional[str] = None + ignore_cert_errors: bool = False + key: Optional[str] = None + proxy: Optional[str] = None + pac: Optional[str] = None + grpc_proxy: Optional[str] = None + ca_cert: Optional[str] = None + syft_command: str = DEFAULT_SYFT_COMMAND + syft_timeout: int = DEFAULT_SYFT_TIMEOUT + only_interim_results: bool = False + + +def create_container_scanner_config_from_args(args) -> ContainerScannerConfig: + return ContainerScannerConfig( + debug=args.debug if 'debug' in args else False, + trace=args.trace if 'trace' in args else False, + quiet=args.quiet if 'quiet' in args else False, + retry=args.retry if 'retry' in args else DEFAULT_RETRY, + timeout=args.timeout if 'timeout' in args else DEFAULT_TIMEOUT, + output=args.output if 'output' in args else None, + format=args.format if 'format' in args else None, + apiurl=args.api2url if 'api2url' in args else None, + proxy=args.proxy if 'proxy' in args else None, + pac=args.pac if 'pac' in args else None, + grpc_proxy=args.grpc_proxy if 'grpc_proxy' in args else None, + ca_cert=args.ca_cert if 'ca_cert' in args else None, + ignore_cert_errors=args.ignore_cert_errors if 'ignore_cert_errors' in args else False, + key=args.key if 'key' in args else None, + syft_command=args.syft_command if 'syft_command' in args else DEFAULT_SYFT_COMMAND, + syft_timeout=args.syft_timeout if 'syft_timeout' in args else DEFAULT_SYFT_TIMEOUT, + ) + + +class LicenseItem(TypedDict): + value: str + spdxExpression: str + type: str + + @classmethod + def from_dict(cls, data: dict): + return cls(**data) + + +class SyftArtifactItem(TypedDict): + name: str + version: str + type: str + purl: str + licenses: List[LicenseItem] + + @classmethod + def from_dict(cls, data: dict): + return cls( + name=data['name'], + version=data['version'], + type=data['type'], + purl=data['purl'], + licenses=[LicenseItem.from_dict(lic) for lic in data['licenses']], + ) + + +class SyftScanResult(TypedDict): + artifacts: List[SyftArtifactItem] + + @classmethod + def from_dict(cls, data: dict): + return cls(artifacts=[SyftArtifactItem.from_dict(a) for a in data['artifacts']]) + + +class PurlItem(TypedDict): + purl: str + requirement: Optional[str] + + +class ContainerScanResultFileItem(TypedDict): + file: str + purls: List[PurlItem] + + +class ContainerScanResult(TypedDict): + files: List[ContainerScanResultFileItem] + + +class DependencyLicenseItem(TypedDict): + value: str + spdxExpression: str + type: str + + +class DependencyItem(TypedDict): + purl: str + licenses: List[DependencyLicenseItem] + + +class DecoratedContainerScanResultFileItem(TypedDict): + file: str + id: str + status: str + dependencies: List[DependencyItem] + + +class DecoratedContainerScanResult(TypedDict): + files: List[ContainerScanResultFileItem] + status: Dict[str, str] + + +class SyftScanError(Exception): + """Base exception for Syft scan errors""" + + pass + + +class SyftExecutionError(SyftScanError): + """Raised when Syft returns a non-zero exit code""" + + pass + + +class SyftJsonError(SyftScanError): + """Raised when Syft output cannot be parsed as JSON""" + + pass + + +class SyftTimeoutError(SyftScanError): + """Raised when a Syft scan times out""" + + pass + + +class SCANOSSDependencyScanError(Exception): + """Base exception for SCANOSS dependency scan errors""" + + pass + + +class DecorateScanResultsError(SCANOSSDependencyScanError): + """Raised when there is an issue decorating scan results with dependencies""" + + pass + + +class ContainerScanner: + """SCANOSS container scanning class. + + This class provides functionality to scan containers using Syft and process + the results into SCANOSS dependency format. + """ + + def __init__( + self, + config: ContainerScannerConfig, + what_to_scan: str, + ): + """Initialize ContainerScanner class. + + Args: + config: ContainerScannerConfig object containing configuration settings. + """ + self.base = ScanossBase( + debug=config.debug, + trace=config.trace, + quiet=config.quiet, + ) + self.presenter = ContainerScannerPresenter( + self, + debug=config.debug, + trace=config.trace, + quiet=config.quiet, + output_file=config.output, + output_format=config.format, + ) + self.grpc_api = ScanossGrpc( + debug=config.debug, + quiet=config.quiet, + trace=config.trace, + url=config.apiurl, + api_key=config.key, + ca_cert=config.ca_cert, + proxy=config.proxy, + pac=config.pac, + grpc_proxy=config.grpc_proxy, + ) + self.what_to_scan: str = what_to_scan + self.syft_command: str = config.syft_command + self.syft_timeout: int = config.syft_timeout + self.only_interim_results: bool = config.only_interim_results + self.syft_output: Optional[SyftScanResult] = None + self.normalized_syft_output: Optional[ContainerScanResult] = None + self.decorated_scan_results: Optional[DecoratedContainerScanResult] = None + + def decorate_scan_results_with_dependencies(self) -> None: + """ + Decorate the scan results with dependencies. + """ + try: + decorated_scan_results = self.grpc_api.get_dependencies(self.normalized_syft_output) + self.decorated_scan_results = decorated_scan_results + return decorated_scan_results + except Exception as e: + error_msg = f'Issue decorating scan results with dependencies: {e}' + self.base.print_stderr(f'ERROR: {error_msg}') + raise DecorateScanResultsError(error_msg) from e + + def scan( + self, + ) -> ContainerScanResult: + """Run a syft scan of the specified target. + + Returns: + ContainerScanResult: The container scan results. + + Raises: + SyftScanError: For other scan-related errors + """ + try: + self.syft_output = self._execute_syft_scan() + self.normalized_syft_output = self._normalize_syft_output() + return self.normalized_syft_output + except Exception as e: + if isinstance(e, SyftScanError): + raise + error_msg = f'Issue running syft scan on {self.what_to_scan}: {e}' + self.base.print_stderr(f'ERROR: {error_msg}') + raise SyftScanError(error_msg) from e + + def _execute_syft_scan(self) -> SyftScanResult: + """ + Execute a Syft scan of the specified target. + + Returns: + SyftScanResult: The result of the Syft scan. + + Raises: + SyftScanError: If the Syft scan fails. + SyftJsonError: If the Syft scan output cannot be parsed as JSON. + SyftTimeoutError: If the Syft scan times out. + SyftExecutionError: If the Syft scan execution fails. + """ + try: + self.base.print_trace(f'About to execute {self.syft_command} scan {self.what_to_scan} -q -o json') + self.base.print_msg('Scanning container...') + result = subprocess.run( + [self.syft_command, 'scan', self.what_to_scan, '-q', '-o', 'json'], + cwd=os.getcwd(), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=self.syft_timeout, + check=False, + ) + self.base.print_trace(f'Subprocess return: {result}') + + if result.returncode: + error_msg = ( + f'Syft scan of {self.what_to_scan} failed with exit code {result.returncode}:\n{result.stdout}' + ) + self.base.print_stderr(f'ERROR: {error_msg}') + raise SyftExecutionError(error_msg) + + try: + json_data = json.loads(result.stdout) + return SyftScanResult.from_dict(json_data) + except json.JSONDecodeError as e: + error_msg = f'Failed to parse JSON output from syft: {e}\n{result.stdout}' + self.base.print_stderr(f'ERROR: {error_msg}') + raise SyftJsonError(error_msg) from e + + except subprocess.TimeoutExpired as e: + error_msg = f'Timed out attempting to run syft scan on {self.what_to_scan}: {e}' + self.base.print_stderr(f'ERROR: {error_msg}') + raise SyftTimeoutError(error_msg) from e + + except Exception as e: + if isinstance(e, SyftScanError): + raise + error_msg = f'Issue running syft scan on {self.what_to_scan}: {e}' + self.base.print_stderr(f'ERROR: {error_msg}') + raise SyftScanError(error_msg) from e + + def _get_dependencies(self) -> None: + """ + Run a dependency scan of the specified target. + """ + try: + if not self.normalized_syft_output: + error_msg = 'Syft scan output is not available' + self.base.print_stderr(error_msg) + raise ValueError(error_msg) + if not self.grpc_api.get_dependencies(self.normalized_syft_output): + error_msg = 'Failed to get dependencies' + self.base.print_stderr(error_msg) + raise SCANOSSDependencyScanError(error_msg) + except Exception as e: + error_msg = f'Failed to run dependency scan: {e}' + self.base.print_stderr(error_msg) + raise SCANOSSDependencyScanError(error_msg) + + def _normalize_syft_output(self) -> ContainerScanResult: + """ + Normalize the Syft output data into the same format we use in dependency scanning + + Returns: + ContainerScanResult: The normalized output + """ + normalized_output = ContainerScanResult() + + # This is a workaround because we don't have file paths as in dependency scanning, we use the container name + file_name = self.what_to_scan + artifacts = self.syft_output['artifacts'] + + unique_purls = set() + unique_purl_items = [] + + for artifact in artifacts: + purl = artifact['purl'] + if purl not in unique_purls: + unique_purls.add(purl) + unique_purl_items.append(PurlItem(purl=purl)) + + normalized_output['files'] = [ + { + 'file': file_name, + 'purls': unique_purl_items, + } + ] + + return normalized_output + + def present(self, output_format: str = None, output_file: str = None): + """Present the results in the selected format""" + self.presenter.present(output_format=output_format, output_file=output_file) + + +class ContainerScannerPresenter(AbstractPresenter): + """ + ContainerScannerPresenter presenter class + Handles the presentation of the container scan results + """ + + def __init__(self, scanner: ContainerScanner, **kwargs): + super().__init__(**kwargs) + self.scanner = scanner + self.AVAILABLE_OUTPUT_FORMATS = ['plain', 'cyclonedx', 'spdxlite', 'csv', 'raw'] + + def _convert_raw_to_scan_output(self) -> dict: + """ + Convert the raw output from dependency scanning API to our scan output format + + Returns: + dict: The converted output + """ + formatted_output = {} + if ( + self.scanner.decorated_scan_results + and 'files' in self.scanner.decorated_scan_results + and self.scanner.decorated_scan_results['files'] + and isinstance(self.scanner.decorated_scan_results['files'], list) + ): + file_item = self.scanner.decorated_scan_results['files'][0] + if file_item and isinstance(file_item, dict) and 'file' in file_item: + formatted_output[file_item['file']] = [file_item] + + return formatted_output + + def _format_plain_output(self) -> str: + """ + Format the scan output data into a plain text string + """ + return json.dumps(self._convert_raw_to_scan_output(), indent=2) + + def _format_raw_output(self) -> str: + """ + Format the scan output data into the raw output from dependency scanning API + """ + if self.scanner.only_interim_results: + return json.dumps(self.scanner.normalized_syft_output, indent=2) + return json.dumps(self.scanner.decorated_scan_results, indent=2) + + def _format_cyclonedx_output(self) -> str: + """ + Format the scan output data into a CycloneDX object + """ + cdx = CycloneDx(self.base.debug, self.output_file) + scan_results = {} + for f in self.scanner.decorated_scan_results['files']: + scan_results[f['file']] = [f] + success, cdx_output = cdx.produce_from_json(scan_results) + if not success: + error_msg = 'Failed to produce CycloneDX output' + self.base.print_stderr(error_msg) + return None + return json.dumps(cdx_output, indent=2) + + def _format_spdxlite_output(self) -> str: + """ + Format the scan output data into a SPDXLite object + """ + spdxlite = SpdxLite(self.base.debug, self.output_file) + scan_results = {} + for f in self.scanner.decorated_scan_results['files']: + scan_results[f['file']] = [f] + if not spdxlite.produce_from_json(scan_results, self.output_file): + error_msg = 'Failed to produce SPDXLite output' + self.base.print_stderr(error_msg) + raise ValueError(error_msg) + + def _format_csv_output(self) -> str: + """ + Format the scan output data into a CSV object + """ + csv = CsvOutput(self.base.debug, self.output_file) + scan_results = {} + for f in self.scanner.decorated_scan_results['files']: + scan_results[f['file']] = [f] + if not csv.produce_from_json(scan_results, self.output_file): + error_msg = 'Failed to produce CSV output' + self.base.print_stderr(error_msg) + raise ValueError(error_msg) + + def _format_json_output(self) -> str: + """ + Format the scan output data into a JSON object + """ + pass diff --git a/src/scanoss/scanners/folder_hasher.py b/src/scanoss/scanners/folder_hasher.py new file mode 100644 index 00000000..549a7c18 --- /dev/null +++ b/src/scanoss/scanners/folder_hasher.py @@ -0,0 +1,358 @@ +import json +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Literal, Optional + +from progress.bar import Bar + +from scanoss.constants import DEFAULT_HFH_DEPTH +from scanoss.file_filters import FileFilters +from scanoss.scanoss_settings import ScanossSettings +from scanoss.scanossbase import ScanossBase +from scanoss.utils.abstract_presenter import AbstractPresenter +from scanoss.utils.crc64 import CRC64 +from scanoss.utils.simhash import WordFeatureSet, fingerprint, simhash, vectorize_bytes + +MINIMUM_FILE_COUNT = 8 +MINIMUM_CONCATENATED_NAME_LENGTH = 32 + +class DirectoryNode: + """ + Represents a node in the directory tree for folder hashing. + """ + + def __init__(self, path: str): + self.path = path + self.is_dir = True + self.children: Dict[str, DirectoryNode] = {} + self.files: List[DirectoryFile] = [] + + +class DirectoryFile: + """ + Represents a file in the directory tree for folder hashing. + """ + + def __init__(self, path: str, key: List[bytes], key_str: str): + self.path = path + self.key = key + self.key_str = key_str + + +@dataclass +class FolderHasherConfig: + debug: bool = False + trace: bool = False + quiet: bool = False + output_file: Optional[str] = None + output_format: Literal['json'] = 'json' + settings_file: Optional[str] = None + skip_settings_file: bool = False + + +def create_folder_hasher_config_from_args(args) -> FolderHasherConfig: + return FolderHasherConfig( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + output_file=getattr(args, 'output', None), + output_format=getattr(args, 'format', 'json'), + settings_file=getattr(args, 'settings', None), + skip_settings_file=getattr(args, 'skip_settings_file', False), + ) + + +class FolderHasher: + """ + Folder Hasher. + + This class is used to produce a folder hash for a given directory. + + It builds a directory tree (DirectoryNode) and computes the associated + hash data for the folder. + + Args: + scan_dir (str): The directory to be hashed. + config (FolderHasherConfig): Configuration parameters for the folder hasher. + scanoss_settings (Optional[ScanossSettings]): Optional settings for Scanoss. + depth (int): How many levels to hash from the root directory (default: 1). + """ + + def __init__( + self, + scan_dir: str, + config: FolderHasherConfig, + scanoss_settings: Optional[ScanossSettings] = None, + depth: int = DEFAULT_HFH_DEPTH, + ): + self.base = ScanossBase( + debug=config.debug, + trace=config.trace, + quiet=config.quiet, + ) + self.file_filters = FileFilters( + debug=config.debug, + trace=config.trace, + quiet=config.quiet, + scanoss_settings=scanoss_settings, + is_folder_hashing_scan=True, + ) + self.presenter = FolderHasherPresenter( + self, + debug=config.debug, + trace=config.trace, + quiet=config.quiet, + ) + + self.scan_dir = scan_dir + self.tree = None + self.depth = depth + + def hash_directory(self, path: str) -> dict: + """ + Generate the folder hashing request structure from a directory path. + + This method builds a directory tree (DirectoryNode) and computes the associated + hash data for the folder. + + Args: + path (str): The root directory path. + + Returns: + dict: The folder hash request structure. + """ + + root_node = self._build_root_node(path) + tree = self._hash_calc_from_node(root_node) + + self.tree = tree + + return tree + + def _build_root_node( + self, + path: str, + ) -> DirectoryNode: + """ + Build a directory tree from the given path with file information. + + The tree includes DirectoryNode objects populated with filtered file items, + each containing their relative path and CRC64 hash key. + + Args: + path (str): The directory path to build the tree from. + + Returns: + DirectoryNode: The root node representing the directory. + """ + root = Path(path).resolve() + root_node = DirectoryNode(str(root)) + + all_files = [ + f for f in root.rglob('*') if f.is_file() + ] + filtered_files = self.file_filters.get_filtered_files_from_files(all_files, str(root)) + + # Sort the files by name to ensure the hash is the same for the same folder + filtered_files.sort() + + bar_ctx = Bar('Hashing files...', max=len(filtered_files)) + + with bar_ctx as bar: + full_file_path = '' + for file_path in filtered_files: + try: + file_path_obj = Path(file_path) if isinstance(file_path, str) else file_path + full_file_path = file_path_obj if file_path_obj.is_absolute() else root / file_path_obj + + self.base.print_debug(f'\nHashing file {str(full_file_path)}') + + file_bytes = full_file_path.read_bytes() + key = CRC64.get_hash_buff(file_bytes) + key_str = ''.join(f'{b:02x}' for b in key) + rel_path = str(full_file_path.relative_to(root)) + + file_item = DirectoryFile(rel_path, key, key_str) + + current_node = root_node + for part in Path(rel_path).parent.parts: + child_path = str(Path(current_node.path) / part) + if child_path not in current_node.children: + current_node.children[child_path] = DirectoryNode(child_path) + current_node = current_node.children[child_path] + current_node.files.append(file_item) + + root_node.files.append(file_item) + + except Exception as e: + self.base.print_debug(f'Skipping file {full_file_path}: {str(e)}') + + bar.next() + return root_node + + def _hash_calc_from_node(self, node: DirectoryNode, current_depth: int = 1) -> dict: + """ + Recursively compute folder hash data for a directory node. + + The hash data includes the path identifier, simhash for file names, + simhash for file content, directory hash, language extensions, and children node hash information. + + Args: + node (DirectoryNode): The directory node to compute the hash for. + current_depth (int): The current depth level (1-based, root is depth 1). + + Returns: + dict: The computed hash data for the node. + """ + hash_data = self._hash_calc(node) + + # Safely calculate relative path + try: + node_path = Path(node.path).resolve() + scan_dir_path = Path(self.scan_dir).resolve() + rel_path = node_path.relative_to(scan_dir_path) + except ValueError: + # If relative_to fails, use the node path as is or a fallback + rel_path = Path(node.path).name if node.path else Path('.') + + # Only process children if we haven't reached the depth limit + children = [] + if current_depth < self.depth: + children = [self._hash_calc_from_node(child, current_depth + 1) for child in node.children.values()] + + return { + 'path_id': str(rel_path), + 'sim_hash_names': f'{hash_data["name_hash"]:02x}' if hash_data['name_hash'] is not None else None, + 'sim_hash_content': f'{hash_data["content_hash"]:02x}' if hash_data['content_hash'] is not None else None, + 'sim_hash_dir_names': f'{hash_data["dir_hash"]:02x}' if hash_data['dir_hash'] is not None else None, + 'lang_extensions': hash_data['lang_extensions'], + 'children': children, + } + + def _hash_calc(self, node: DirectoryNode) -> dict: + """ + Compute folder hash values for a given directory node. + + The method aggregates unique file keys and sorted file names to generate + simhash-based hash values for both file names and file contents. + + The most significant byte of the name simhash is then replaced by a computed head value. + + Args: + node (DirectoryNode): The directory node containing file items. + + Returns: + dict: A dictionary with 'name_hash', 'content_hash', 'dir_hash', and 'lang_extensions' keys. + """ + processed_hashes = set() + unique_file_names = set() + unique_directories = set() + extension_map = {} + file_hashes = [] + selected_names = [] + + for file in node.files: + key_str = file.key_str + + file_name = os.path.basename(file.path) + + file_name_without_extension, extension = os.path.splitext(file_name) + current_directory = os.path.dirname(file.path) + + if extension and len(extension) > 1: + ext_without_dot = extension[1:] + extension_map[ext_without_dot] = extension_map.get(ext_without_dot, 0) + 1 + + current_directory.replace(self.scan_dir, '', 1).lstrip(os.path.sep) + parts = current_directory.split(os.path.sep) + for d in parts: + if d in {'', '.', '..'}: + continue + unique_directories.add(d) + + processed_hashes.add(key_str) + unique_file_names.add(file_name_without_extension) + selected_names.append(file_name) + file_hashes.append(file.key) + + if len(selected_names) < MINIMUM_FILE_COUNT: + return {'name_hash': None, 'content_hash': None, 'dir_hash': None, 'lang_extensions': None} + + selected_names.sort() + concatenated_names = ''.join(selected_names) + + if len(concatenated_names.encode('utf-8')) < MINIMUM_CONCATENATED_NAME_LENGTH: + return {'name_hash': None, 'content_hash': None, 'dir_hash': None, 'lang_extensions': None} + + # Concatenate the unique file names without the extensions, adding a space and sorting them alphabetically + unique_file_names_list = list(unique_file_names) + unique_file_names_list.sort() + concatenated_names = ' '.join(unique_file_names_list) + + # We do the same for the directory names, adding a space and sorting them alphabetically + unique_directories_list = list(unique_directories) + unique_directories_list.sort() + concatenated_directories = ' '.join(unique_directories_list) + + names_simhash = simhash(WordFeatureSet(concatenated_names.encode('utf-8'))) + dir_simhash = simhash(WordFeatureSet(concatenated_directories.encode('utf-8'))) + content_simhash = fingerprint(vectorize_bytes(file_hashes)) + + # Debug logging similar to Go implementation + self.base.print_debug(f'Unique file names: {unique_file_names_list}') + self.base.print_debug(f'Unique directories: {unique_directories_list}') + self.base.print_debug(f'{dir_simhash:x}/{names_simhash:x} - {content_simhash:x} - {extension_map}') + + return { + 'name_hash': names_simhash, + 'content_hash': content_simhash, + 'dir_hash': dir_simhash, + 'lang_extensions': extension_map, + } + + def present(self, output_format: Optional[str] = None, output_file: Optional[str] = None): + """Present the hashed tree in the selected format""" + self.presenter.present(output_format=output_format, output_file=output_file) + + +class FolderHasherPresenter(AbstractPresenter): + """ + FolderHasher presenter class + Handles the presentation of the folder hashing scan results + """ + + def __init__(self, folder_hasher: FolderHasher, **kwargs): + super().__init__(**kwargs) + self.folder_hasher = folder_hasher + + def _format_json_output(self) -> str: + """ + Format the scan output data into a JSON object + + Returns: + str: The formatted JSON string + """ + return json.dumps(self.folder_hasher.tree, indent=2) + + def _format_plain_output(self) -> str: + """ + Format the scan output data into a plain text string + """ + return ( + json.dumps(self.folder_hasher.tree, indent=2) + if isinstance(self.folder_hasher.tree, dict) + else str(self.folder_hasher.tree) + ) + + def _format_cyclonedx_output(self) -> str: + raise NotImplementedError('CycloneDX output is not implemented') + + def _format_spdxlite_output(self) -> str: + raise NotImplementedError('SPDXlite output is not implemented') + + def _format_csv_output(self) -> str: + raise NotImplementedError('CSV output is not implemented') + + def _format_raw_output(self) -> str: + raise NotImplementedError('Raw output is not implemented') diff --git a/src/scanoss/scanners/scanner_config.py b/src/scanoss/scanners/scanner_config.py new file mode 100644 index 00000000..ed48342d --- /dev/null +++ b/src/scanoss/scanners/scanner_config.py @@ -0,0 +1,73 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +from dataclasses import dataclass +from typing import Optional + +from pypac.parser import PACFile + +from scanoss.constants import ( + DEFAULT_NB_THREADS, + DEFAULT_POST_SIZE, + DEFAULT_SC_TIMEOUT, + DEFAULT_TIMEOUT, +) + + +@dataclass +class ScannerConfig: + debug: bool = False + trace: bool = False + quiet: bool = False + api_key: Optional[str] = None + url: Optional[str] = None + grpc_url: Optional[str] = None + post_size: int = DEFAULT_POST_SIZE + timeout: int = DEFAULT_TIMEOUT + sc_timeout: int = DEFAULT_SC_TIMEOUT + nb_threads: int = DEFAULT_NB_THREADS + proxy: Optional[str] = None + grpc_proxy: Optional[str] = None + + ca_cert: Optional[str] = None + pac: Optional[PACFile] = None + + +def create_scanner_config_from_args(args) -> ScannerConfig: + return ScannerConfig( + debug=args.debug, + trace=args.trace, + quiet=args.quiet, + api_key=getattr(args, 'key', None), + url=getattr(args, 'api_url', None), + grpc_url=getattr(args, 'grpc_url', None), + post_size=getattr(args, 'post_size', DEFAULT_POST_SIZE), + timeout=getattr(args, 'timeout', DEFAULT_TIMEOUT), + sc_timeout=getattr(args, 'sc_timeout', DEFAULT_SC_TIMEOUT), + nb_threads=getattr(args, 'nb_threads', DEFAULT_NB_THREADS), + proxy=getattr(args, 'proxy', None), + grpc_proxy=getattr(args, 'grpc_proxy', None), + ca_cert=getattr(args, 'ca_cert', None), + pac=getattr(args, 'pac', None), + ) diff --git a/src/scanoss/scanners/scanner_hfh.py b/src/scanoss/scanners/scanner_hfh.py new file mode 100644 index 00000000..06deca97 --- /dev/null +++ b/src/scanoss/scanners/scanner_hfh.py @@ -0,0 +1,543 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +import hashlib +import json +import os +import threading +import time +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +from packageurl.contrib import purl2url +from progress.spinner import Spinner + +from scanoss.constants import ( + DEFAULT_HFH_DEPTH, + DEFAULT_HFH_MIN_ACCEPTED_SCORE, + DEFAULT_HFH_RANK_THRESHOLD, + DEFAULT_HFH_RECURSIVE_THRESHOLD, +) +from scanoss.cyclonedx import CycloneDx +from scanoss.file_filters import FileFilters +from scanoss.scanners.folder_hasher import FolderHasher +from scanoss.scanners.scanner_config import ScannerConfig +from scanoss.scanoss_settings import ScanossSettings +from scanoss.scanossbase import ScanossBase +from scanoss.scanossgrpc import ScanossGrpc +from scanoss.utils.abstract_presenter import AbstractPresenter + + +class ScannerHFH: + """ + Folder Hashing Scanner. + + This scanner processes a directory, computes CRC64 hashes for the files, + and calculates simhash values based on file names and content to detect folder-level similarities. + """ + + def __init__( # noqa: PLR0913 + self, + scan_dir: str, + config: ScannerConfig, + client: Optional[ScanossGrpc] = None, + scanoss_settings: Optional[ScanossSettings] = None, + rank_threshold: int = DEFAULT_HFH_RANK_THRESHOLD, + depth: int = DEFAULT_HFH_DEPTH, + recursive_threshold: float = DEFAULT_HFH_RECURSIVE_THRESHOLD, + min_accepted_score: float = DEFAULT_HFH_MIN_ACCEPTED_SCORE, + use_grpc: bool = False, + ): + """ + Initialize the ScannerHFH. + + Args: + scan_dir (str): The directory to be scanned. + config (ScannerConfig): Configuration parameters for the scanner. + client (ScanossGrpc): gRPC client for communicating with the scanning service. + scanoss_settings (Optional[ScanossSettings]): Optional settings for Scanoss. + rank_threshold (int): Get results with rank below this threshold (default: 5). + depth (int): How many levels to scan (default: 1). + recursive_threshold (float): Minimum score threshold to consider a match (default: 0.25). + min_accepted_score (float): Only show results with a score at or above this threshold (default: 0.15). + """ + self.base = ScanossBase( + debug=config.debug, + trace=config.trace, + quiet=config.quiet, + ) + self.presenter = ScannerHFHPresenter( + self, + debug=config.debug, + trace=config.trace, + quiet=config.quiet, + ) + self.file_filters = FileFilters( + debug=config.debug, + trace=config.trace, + quiet=config.quiet, + scanoss_settings=scanoss_settings, + ) + self.folder_hasher = FolderHasher( + scan_dir=scan_dir, + config=config, + scanoss_settings=scanoss_settings, + depth=depth, + ) + + self.scan_dir = scan_dir + self.client = client + self.scan_results = None + self.rank_threshold = rank_threshold + self.recursive_threshold = recursive_threshold + self.min_accepted_score = min_accepted_score + self.use_grpc = use_grpc + + def _execute_grpc_scan(self, hfh_request: Dict) -> None: + """ + Execute folder hash scan and decorate results with license information. + + Args: + hfh_request: Request dictionary for the gRPC call + """ + try: + self.scan_results = self.client.folder_hash_scan(hfh_request, self.use_grpc) + self._decorate_with_licenses() + except Exception as e: + self.base.print_stderr(f'Error during folder hash scan: {e}') + self.scan_results = None + + def _decorate_with_licenses(self) -> None: + """ + Decorate each component version in scan results with license information + by calling the dependency service. + """ + if not self.scan_results or not self.client: + return + results = self.scan_results.get('results', []) + if not results: + return + + dep_files = self._collect_dep_files(results) + if not dep_files: + return + + try: + decorated = self.client.get_dependencies({'files': dep_files}) + except Exception as e: + self.base.print_stderr(f'Warning: Failed to fetch license data: {e}') + return + + if not decorated or 'files' not in decorated: + return + + license_map = self._build_license_map(decorated) + self._inject_licenses(results, license_map) + + @staticmethod + def _collect_dep_files(results: List[Dict]) -> List[Dict]: + """Collect dependency file entries for all component versions in the results.""" + dep_files = [] + for result in results: + path_id = result.get('path_id', '') + for component in result.get('components', []): + purl = component.get('purl', '') + if not purl: + continue + for version_entry in component.get('versions', []): + version = version_entry.get('version', '') + if not version: + continue + dep_files.append({ + 'file': path_id, + 'purls': [{'purl': purl, 'requirement': version}], + }) + return dep_files + + @staticmethod + def _build_license_map(decorated: Dict) -> Dict[str, List]: + """Build a purl@requirement -> licenses lookup from the dependency service response. + + Args: + decorated (Dict): The response from the dependency service containing + decorated files with license information. + + Returns: + Dict[str, List]: A mapping of 'purl@requirement' keys to their + corresponding list of license dictionaries. + """ + license_map = {} + for dep_file in decorated.get('files', []): + for dep in dep_file.get('dependencies', []): + dep_purl = dep.get('purl', '') + # Use 'requirement' instead of 'version' as the key because the service + # may resolve a different version, but the requirement always matches what was sent. + dep_requirement = dep.get('requirement', '') + licenses = dep.get('licenses', []) + if dep_purl and licenses: + license_map[f'{dep_purl}@{dep_requirement}'] = licenses + return license_map + + @staticmethod + def _inject_licenses(results: List[Dict], license_map: Dict[str, List]) -> None: + """Inject licenses from the lookup map into each component version entry. + + Args: + results (List[Dict]): The 'results' list from the HFH scan response. + Each result contains components with version entries that will + be mutated in place to include license data. + license_map (Dict[str, List]): A mapping of 'purl@version' keys to + their corresponding list of license dictionaries, as built by + ``_build_license_map``. + """ + for result in results: + for component in result.get('components', []): + purl = component.get('purl', '') + for version_entry in component.get('versions', []): + version = version_entry.get('version', '') + version_entry['licenses'] = license_map.get(f'{purl}@{version}', []) + + def scan(self) -> Optional[Dict]: + """ + Scan the provided directory using the folder hashing algorithm. + + Returns: + Optional[Dict]: The folder hash response from the gRPC client, or None if an error occurs. + """ + hfh_request = { + 'root': self.folder_hasher.hash_directory(path=self.scan_dir), + 'rank_threshold': self.rank_threshold, + 'recursive_threshold': self.recursive_threshold, + 'min_accepted_score': self.min_accepted_score, + } + + spinner_ctx = Spinner('Scanning folder...') + + with spinner_ctx as spinner: + grpc_thread = threading.Thread(target=self._execute_grpc_scan, args=(hfh_request,)) + grpc_thread.start() + + while grpc_thread.is_alive(): + spinner.next() + time.sleep(0.1) + + grpc_thread.join() + + return self.scan_results + + def present(self, output_format: str = None, output_file: str = None): + """Present the results in the selected format""" + self.presenter.present(output_format=output_format, output_file=output_file) + + +class ScannerHFHPresenter(AbstractPresenter): + """ + ScannerHFH presenter class + Handles the presentation of the folder hashing scan results + """ + + def __init__(self, scanner: ScannerHFH, **kwargs): + """ + Initialize the presenter. + + Args: + scanner (ScannerHFH): The HFH scanner instance containing scan results and file filters. + **kwargs: Additional arguments passed to AbstractPresenter (debug, trace, quiet, etc.). + """ + super().__init__(**kwargs) + self.scanner = scanner + + def _format_json_output(self) -> str: + """ + Format the scan output data into a JSON object + + Returns: + str: The formatted JSON string + """ + return json.dumps(self.scanner.scan_results, indent=2) + + def _format_plain_output(self) -> str: + """ + Format the scan output data into a plain text string + """ + return ( + json.dumps(self.scanner.scan_results, indent=2) + if isinstance(self.scanner.scan_results, dict) + else str(self.scanner.scan_results) + ) + + def _format_cyclonedx_output(self) -> str: # noqa: PLR0911 + if not self.scanner.scan_results: + return '' + try: + if 'results' not in self.scanner.scan_results or not self.scanner.scan_results['results']: + self.base.print_stderr('ERROR: No scan results found') + return '' + + first_result = self.scanner.scan_results['results'][0] + + best_match_components = [c for c in first_result.get('components', []) if c.get('order') == 1] + if not best_match_components: + self.base.print_stderr('ERROR: No best match component found') + return '' + + best_match_component = best_match_components[0] + if not best_match_component.get('versions'): + self.base.print_stderr('ERROR: No versions found for best match component') + return '' + best_match_version = best_match_component['versions'][0] + purl = best_match_component['purl'] + version = best_match_version['version'] + licenses = best_match_version.get('licenses', []) + + # Build scan_results from already-decorated HFH data + scan_results = { + f'{best_match_component["name"]}:{version}': [ + { + 'id': 'dependency', + 'dependencies': [ + { + 'purl': purl, + 'component': best_match_component.get('name', ''), + 'version': version, + 'licenses': licenses, + } + ], + } + ] + } + + get_vulnerabilities_json_request = { + 'components': [{'purl': purl, 'requirement': version}], + } + vulnerabilities = self.scanner.client.get_vulnerabilities_json(get_vulnerabilities_json_request) + + cdx = CycloneDx(self.base.debug) + success, cdx_output = cdx.produce_from_json(scan_results, print_output=False) + if not success: + error_msg = 'ERROR: Failed to produce CycloneDX output' + self.base.print_stderr(error_msg) + return None + + if vulnerabilities: + cdx_output = cdx.append_vulnerabilities(cdx_output, vulnerabilities, purl) + + return json.dumps(cdx_output, indent=2) + except Exception as e: + self.base.print_stderr(f'ERROR: Failed to produce CycloneDX output: {e}') + return None + + def _format_spdxlite_output(self) -> str: + raise NotImplementedError('SPDXlite output is not implemented') + + def _format_csv_output(self) -> str: + raise NotImplementedError('CSV output is not implemented') + + def _format_raw_output(self) -> str: + """ + Convert HFH scan results into snippet-scanner JSON format. + + Expands directory-level HFH results into per-file entries keyed by + relative file path, matching the structure returned by the snippet scanner. + For each file, computes the MD5 hash and constructs the file_url using + the API base URL from the scanner config. + + Returns: + str: A JSON string with the snippet-scanner format, or '{}' if no results. + """ + if not self.scanner.scan_results or 'results' not in self.scanner.scan_results: + return '{}' + + hfh_results = self.scanner.scan_results.get('results', []) + if not hfh_results: + return '{}' + + # Collect best-match component info per path_id + path_components = self._extract_best_components(hfh_results) + if not path_components: + return '{}' + + # Get all filtered files once (relative paths to scan_dir) + all_files = self.scanner.file_filters.get_filtered_files_from_folder(self.scanner.scan_dir) + + # Sort path_ids by depth (deepest first) so most-specific match wins. + # Root path '.' is always last (-1), others sort by separator count then path length. + # Example with path_ids: ['.', 'external', 'project-1.0', 'project-1.0/src/lib'] + # Sorted result: ['project-1.0/src/lib', 'project-1.0', 'external', '.'] + # - 'project-1.0/src/lib' (depth 2) claims its files first + # - 'project-1.0' (depth 0, len 11) claims remaining files under it + # - 'external' (depth 0, len 8) claims external/ files + # - '.' (root, always last) picks up everything else + sorted_path_ids = sorted( + path_components.keys(), + key=lambda p: (-1, 0) if p == '.' else (p.count(os.sep), len(p)), + reverse=True, + ) + + output = {} + claimed_files = set() + scan_dir = Path(self.scanner.scan_dir).resolve() + + for path_id in sorted_path_ids: + component, best_version = path_components[path_id] + for file_path in all_files: + if file_path in claimed_files: + continue + if not self._file_matches_path_id(file_path, path_id): + continue + + claimed_files.add(file_path) + # Path.__truediv__ (/) joins paths using the correct OS separator + file_hash = self._compute_file_md5(scan_dir / file_path) + api_url = self.scanner.client.orig_url or '' + entry = self._build_file_match_entry(component, best_version, file_path, file_hash, api_url) + output[file_path] = [entry] + + return json.dumps(output, indent=2) + + @staticmethod + def _extract_best_components(hfh_results: List[Dict]) -> Dict[str, Tuple[Dict, Dict]]: + """ + Extract the best-match component and version for each path_id from HFH results. + + Filters for components with order == 1 (best match) and takes their first version. + Results without a qualifying component or without versions are skipped. + + Args: + hfh_results (List[Dict]): The 'results' list from the HFH API response. + + Returns: + Dict[str, Tuple[Dict, Dict]]: A dict mapping path_id to (component, best_version). + """ + path_components = {} + for result in hfh_results: + path_id = result.get('path_id', '.') + components = result.get('components', []) + best = [c for c in components if c.get('order') == 1] + if not best: + continue + component = best[0] + versions = component.get('versions', []) + if not versions: + continue + path_components[path_id] = (component, versions[0]) + return path_components + + @staticmethod + def _file_matches_path_id(file_path: str, path_id: str) -> bool: + """ + Check if a file path belongs under a given path_id directory. + + Both file_path and path_id are relative to the scan root directory. + A path_id of '.' matches all files (root directory). + + Args: + file_path (str): Relative file path from the scan root. + path_id (str): Relative directory path from the HFH result. + + Returns: + bool: True if the file is under the given path_id directory. + """ + if path_id == '.': + return True + # file_path and path_id are both relative to scan_dir + return file_path == path_id or file_path.startswith(path_id + os.sep) + + def _compute_file_md5(self, file_path: Path) -> str: + """ + Compute the MD5 hash of a file's contents. + + Uses the same approach as the snippet scanner (winnowing.py) to ensure + consistent file_hash values across scan types. + + Args: + file_path (Path): Absolute path to the file. + + Returns: + str: The MD5 hex digest, or an empty string if the file cannot be read. + """ + try: + return hashlib.md5(file_path.read_bytes()).hexdigest() + except (OSError, IOError) as e: + self.base.print_stderr(f'Warning: Failed to compute MD5 for {file_path}: {e}') + return '' + + @staticmethod + def _build_file_match_entry( + component: Dict, best_version: Dict, file_path: str, file_hash: str, base_url: str, + ) -> Dict: + """ + Build a snippet-scanner-compatible result entry from an HFH component. + + Maps HFH component fields to the standard scan result format. Fields not + available from HFH (url_hash, release_date, licenses) are included as empty + values since downstream validators require them. + + Args: + component (Dict): The HFH component with purl, name, vendor fields. + best_version (Dict): The top version entry with version and score fields. + file_path (str): Relative file path from the scan root directory. + file_hash (str): Pre-computed MD5 hash of the local file. + base_url (str): API base URL used to construct the file_url field. + + Returns: + Dict: A result entry compatible with the snippet-scanner JSON format. + """ + purl = component.get('purl', '') + version = best_version.get('version', '') + licenses = [ + { + 'name': lic.get('spdx_id') or lic.get('name', ''), + 'patent_hints': '', + 'copyleft': '', + 'checklist_url': '', + 'incompatible_with': '', + 'osadl_updated': '', + 'source': 'component_declared', + 'url': f"https://spdx.org/licenses/{lic['spdx_id']}.html" if lic.get('spdx_id') else '', + } + for lic in best_version.get('licenses', []) + ] + + url = purl2url.get_repo_url(purl) if purl else '' + return { + 'id': 'file', + 'matched': '100%', + 'purl': [purl], + 'component': component.get('name', ''), + 'vendor': component.get('vendor', ''), + 'version': version, + 'latest': version, + 'url': url or '', + 'file': file_path, + 'file_hash': file_hash, + 'file_url': f'{base_url}/file_contents/{file_hash}', + 'source_hash': file_hash, + 'url_hash': '', + 'release_date': '', + 'licenses': licenses, + 'lines': 'all', + 'oss_lines': 'all', + 'status': 'pending', + } diff --git a/src/scanoss/scanoss_settings.py b/src/scanoss/scanoss_settings.py new file mode 100644 index 00000000..7a39fbaa --- /dev/null +++ b/src/scanoss/scanoss_settings.py @@ -0,0 +1,593 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2024, SCANOSS + + 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. +""" + +import json +from dataclasses import dataclass +from pathlib import Path +from typing import List, Optional, TypedDict + +import importlib_resources +from jsonschema import validate + +from .scanossbase import ScanossBase +from .utils.file import ( + JSON_ERROR_FILE_EMPTY, + JSON_ERROR_FILE_NOT_FOUND, + validate_json_file, +) + +DEFAULT_SCANOSS_JSON_FILE = Path('scanoss.json') + + +@dataclass +class BomEntry: + purl: Optional[str] = None + path: Optional[str] = None + comment: Optional[str] = None + + @classmethod + def from_dict(cls, data: dict) -> 'BomEntry': + raw_path = data.get('path') + path = raw_path.rstrip('/') if raw_path else None + return cls( + purl=data.get('purl'), + path=path, + comment=data.get('comment'), + ) + + def matches_path(self, result_path: str) -> bool: + """ + Check if this entry's path matches a result path. + + Args: + result_path: Path from the scan result + + Returns: + True if this entry's path matches the result path + """ + if not self.path: + return True + path = self.path.rstrip('/') + return path == result_path or result_path.startswith(path + '/') + + @property + def priority(self) -> int: + """ + Priority score for this BOM entry. Higher score means higher priority (more specific). + + Score 4: both path and purl (most specific) + Score 2: purl only + Score 1: path only (remove only, no purl) + + Returns: + Priority score + """ + has_path = bool(self.path) + has_purl = bool(self.purl) + if has_path and has_purl: + return 4 + if has_purl: + return 2 + if has_path: + return 1 + return 0 + + +@dataclass +class ReplaceRule(BomEntry): + replace_with: Optional[str] = None + license: Optional[str] = None + + @classmethod + def from_dict(cls, data: dict) -> 'ReplaceRule': + if not data.get('replace_with'): + raise ValueError(f'Replace rule missing "replace_with" (purl: {data.get("purl")})') + raw_path = data.get('path') + path = raw_path.rstrip('/') if raw_path else None + return cls( + purl=data.get('purl'), + path=path, + comment=data.get('comment'), + replace_with=data.get('replace_with'), + license=data.get('license'), + ) + + +@dataclass(frozen=True) +class SbomContext: + """SBOM context for a file or batch of files.""" + + purls: tuple # Use tuple for hashability (frozen dataclass) + scan_type: Optional[str] + + def to_payload(self) -> Optional[dict]: + """Convert to API payload dict.""" + if not self.purls or not self.scan_type: + return None + components = [{'purl': p} for p in self.purls] + return { + 'assets': json.dumps({'components': components}), + 'scan_type': self.scan_type, + } + + @classmethod + def empty(cls) -> 'SbomContext': + """Return empty context (no purls, no scan_type).""" + return cls(purls=(), scan_type=None) + + + +def find_best_match(result_path: str, result_purls: List[str], entries: List[BomEntry]) -> Optional[BomEntry]: + """ + Find the highest-priority BOM entry that matches a result. + When scores are equal, the longer path wins (more specific). + + Args: + result_path: Path from the scan result + result_purls: List of purls from the scan result + entries: List of BOM entries to check + + Returns: + The best matching BOM entry, or None if no match + """ + best_entry = None + best_score = -1 + best_path_len = -1 + + for entry in entries: + entry_path = entry.path or '' + entry_purl = entry.purl or '' + + if not entry_path and not entry_purl: + continue + if entry_path and not entry.matches_path(result_path): + continue + if entry_purl and (not result_purls or entry_purl not in result_purls): + continue + + score = entry.priority + path_len = len(entry_path) + if score > best_score or (score == best_score and path_len > best_path_len): + best_entry = entry + best_score = score + best_path_len = path_len + + return best_entry + + +class SizeFilter(TypedDict, total=False): + patterns: List[str] + min: int + max: int + + +class ScanossSettingsError(Exception): + pass + + +def _load_settings_schema() -> dict: + """ + Load the SCANOSS settings schema from a JSON file. + + Returns: + dict: The parsed JSON content of the SCANOSS settings schema. + + Raises: + ScanossSettingsError: If there is any issue in locating, reading, or parsing the JSON file + """ + try: + schema_path = importlib_resources.files(__name__) / 'data' / 'scanoss-settings-schema.json' + with importlib_resources.as_file(schema_path) as f: + with open(f, 'r', encoding='utf-8') as file: + return json.load(file) + except Exception as e: + raise ScanossSettingsError(f'ERROR: Problem parsing Scanoss Settings Schema JSON file: {e}') from e + + +class ScanossSettings(ScanossBase): + """ + Handles the loading and parsing of the SCANOSS settings file + """ + + def __init__( + self, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + filepath: 'str | None' = None, + ): + """ + Args: + debug (bool, optional): Debug. Defaults to False. + trace (bool, optional): Trace. Defaults to False. + quiet (bool, optional): Quiet. Defaults to False. + filepath (str, optional): Path to settings file. Defaults to None. + """ + super().__init__(debug, trace, quiet) + self.data = {} + self.settings_file_type = None + self.scan_type = None + self.schema = _load_settings_schema() + if filepath: + self.load_json_file(filepath) + + def load_json_file(self, filepath: Optional[str] = None, scan_root: Optional[str] = None) -> 'ScanossSettings': + """ + Load the scan settings file. If no filepath is provided, scanoss.json will be used as default. + + Args: + filepath (str): Path to the SCANOSS settings file + """ + + if not filepath: + filepath = DEFAULT_SCANOSS_JSON_FILE + + filepath = Path(scan_root) / filepath if scan_root else Path(filepath) + + json_file = filepath.resolve() + + if filepath == DEFAULT_SCANOSS_JSON_FILE and not json_file.exists(): + self.print_debug(f'Default settings file "{filepath}" not found. Skipping...') + return self + self.print_msg(f'Loading settings file {filepath}...') + + result = validate_json_file(json_file) + if not result.is_valid: + if result.error_code in (JSON_ERROR_FILE_NOT_FOUND, JSON_ERROR_FILE_EMPTY): + self.print_msg( + f'WARNING: The supplied settings file "{filepath}" was not found or is empty. Skipping...' + ) + return self + else: + raise ScanossSettingsError(f'Problem with settings file. {result.error}') + try: + validate(result.data, self.schema) + except Exception as e: + raise ScanossSettingsError(f'Invalid settings file. {e}') from e + self.data = result.data + self.print_debug(f'Loading scan settings from: {filepath}') + return self + + def set_file_type(self, file_type: str): + """ + Set the file type in order to support both legacy SBOM.json and new scanoss.json files + Args: + file_type (str): 'legacy' or 'new' + + Raises: + Exception: Invalid scan settings file, missing "components" or "bom" + """ + self.settings_file_type = file_type + if not self._is_valid_sbom_file: + raise Exception('Invalid scan settings file, missing "components" or "bom")') + return self + + def set_scan_type(self, scan_type: str): + """ + Set the scan type to support legacy SBOM.json files + Args: + scan_type (str): 'identify' or 'exclude' + """ + self.scan_type = scan_type + return self + + def _is_valid_sbom_file(self): + """ + Check if the scan settings file is valid + Returns: + bool: True if the file is valid, False otherwise + """ + if not self.data.get('components') or not self.data.get('bom'): + return False + return True + + def _get_bom(self): + """ + Get the Bill of Materials from the settings file + Returns: + dict: If using scanoss.json + list: If using SBOM.json + """ + if self.settings_file_type == 'legacy': + if isinstance(self.data, list): + return self.data + elif isinstance(self.data, dict) and self.data.get('components'): + return self.data.get('components') + else: + return [] + return self.data.get('bom', {}) + + def get_bom_include(self) -> List[BomEntry]: + """ + Get the list of components to include in the scan + Returns: + list: List of components to include in the scan + """ + if self.settings_file_type == 'legacy': + raw = self._get_bom() + else: + raw = self._get_bom().get('include', []) + return [BomEntry.from_dict(entry) for entry in raw] + + def get_bom_exclude(self) -> List[BomEntry]: + """ + Get the list of components to exclude from the scan + Returns: + list: List of components to exclude from the scan + """ + if self.settings_file_type == 'legacy': + raw = self._get_bom() + else: + raw = self._get_bom().get('exclude', []) + return [BomEntry.from_dict(entry) for entry in raw] + + def get_bom_remove(self) -> List[BomEntry]: + """ + Get the list of components to remove from the scan + Returns: + list: List of components to remove from the scan + """ + if self.settings_file_type == 'legacy': + raw = self._get_bom() + else: + raw = self._get_bom().get('remove', []) + return [BomEntry.from_dict(entry) for entry in raw] + + def get_bom_replace(self) -> List[ReplaceRule]: + """ + Get the list of components to replace in the scan + Returns: + list: List of replace rules + """ + if self.settings_file_type == 'legacy': + return [] + raw = self._get_bom().get('replace', []) + rules = [] + for entry in raw: + try: + rules.append(ReplaceRule.from_dict(entry)) + except ValueError as e: + self.print_stderr(f'WARNING: {e}. Skipping.') + return rules + + def _get_purls_for_path(self, file_path: str, entries: List[BomEntry]) -> list: + """ + Extract matching purls from entries for a given file path. + + Purl-only entries (no path) are always included. + Path-scoped entries are included only if the file matches. + When multiple rules match the same purl, the highest specificity is used. + + Args: + file_path: File path to check + entries: List of BomEntry to check against + + Returns: + List of purl strings ordered by specificity (most specific first) + """ + if not entries: + return [] + + purl_scores = {} + for entry in entries: + entry_purl = entry.purl or '' + if not entry_purl: + continue + entry_path = entry.path or '' + if not entry_path or entry.matches_path(file_path): + # Score: priority (0-4) + path length (longer = more specific) + score = entry.priority + len(entry_path) + if entry_purl not in purl_scores or score > purl_scores[entry_purl]: + purl_scores[entry_purl] = score + + # Sort by score descending (most specific first) + return sorted(purl_scores.keys(), key=lambda p: -purl_scores[p]) + + def _get_replace_with_purls_for_path(self, file_path: str, entries: List[ReplaceRule]) -> list: + """ + Extract replace_with purls from replace rules matching a given file path. + + At scan time we don't know the result PURLs yet, so only path matching + is used. Global rules (no path) always contribute their replace_with. + + Args: + file_path: File path to check + entries: List of ReplaceRule to check against + + Returns: + List of replace_with purl strings ordered by specificity (most specific first) + """ + if not entries: + return [] + + purl_scores = {} + for entry in entries: + if entry.matches_path(file_path): + entry_path = entry.path or '' + score = entry.priority + len(entry_path) + if entry.replace_with not in purl_scores or score > purl_scores[entry.replace_with]: + purl_scores[entry.replace_with] = score + + return sorted(purl_scores.keys(), key=lambda p: -purl_scores[p]) + + def get_sbom_context(self, file_path: str) -> SbomContext: + """ + Get SBOM context matching a file path. + + Logic: + - Legacy files: use self.scan_type set during file load + - New format: try include rules first, fall back to exclude rules + + Args: + file_path: File path to check + + Returns: + SbomContext with purls ordered by specificity + """ + if not self.data: + return SbomContext.empty() + + # Legacy files: use self.scan_type set during file load (--identify or --ignore flag) + if self.is_legacy(): + raw = self._get_bom() + entries = [BomEntry.from_dict(entry) for entry in raw] + purls = self._get_purls_for_path(file_path, entries) + if purls: + return SbomContext(purls=tuple(purls), scan_type=self.scan_type) + return SbomContext.empty() + + # Collect include + replace_with purls, fall back to exclude + include_purls = self._get_purls_for_path(file_path, self.get_bom_include()) + replace_purls = self._get_replace_with_purls_for_path(file_path, self.get_bom_replace()) + + # Merge include + replace_with, deduplicating (include order first) + all_identify = list(dict.fromkeys(include_purls + replace_purls)) + if all_identify: + return SbomContext(purls=tuple(all_identify), scan_type='identify') + + exclude_purls = self._get_purls_for_path(file_path, self.get_bom_exclude()) + if exclude_purls: + return SbomContext(purls=tuple(exclude_purls), scan_type='blacklist') + + return SbomContext.empty() + + def is_legacy(self): + """Check if the settings file is legacy""" + return self.settings_file_type == 'legacy' + + def get_skip_patterns(self, operation_type: str) -> List[str]: + """ + Get the list of patterns to skip based on the operation type + Args: + operation_type (str): Operation type + Returns: + List: List of patterns to skip + """ + return self.data.get('settings', {}).get('skip', {}).get('patterns', {}).get(operation_type, []) + + def get_skip_sizes(self, operation_type: str) -> List[SizeFilter]: + """ + Get the min and max sizes to skip based on the operation type + Args: + operation_type (str): Operation type + Returns: + List: Min and max sizes to skip + """ + return self.data.get('settings', {}).get('skip', {}).get('sizes', {}).get(operation_type, []) + + def get_file_snippet_settings(self) -> dict: + """ + Get the file_snippet settings section + Returns: + dict: File snippet settings + """ + return self.data.get('settings', {}).get('file_snippet', {}) + + def get_min_snippet_hits(self) -> Optional[int]: + """ + Get the minimum snippet hits required + Returns: + int or None: Minimum snippet hits, or None if not set + """ + return self.get_file_snippet_settings().get('min_snippet_hits') + + def get_min_snippet_lines(self) -> Optional[int]: + """ + Get the minimum snippet lines required + Returns: + int or None: Minimum snippet lines, or None if not set + """ + return self.get_file_snippet_settings().get('min_snippet_lines') + + def get_ranking_enabled(self) -> Optional[bool]: + """ + Get whether ranking is enabled + Returns: + bool or None: True if enabled, False if disabled, None if not set + """ + return self.get_file_snippet_settings().get('ranking_enabled') + + def get_ranking_threshold(self) -> Optional[int]: + """ + Get the ranking threshold value + Returns: + int or None: Ranking threshold, or None if not set + """ + return self.get_file_snippet_settings().get('ranking_threshold') + + def get_honour_file_exts(self) -> Optional[bool]: + """ + Get whether to honour file extensions + Returns: + bool or None: True to honour, False to ignore, None if not set + """ + return self.get_file_snippet_settings().get('honour_file_exts') + + def get_skip_headers_limit(self) -> int: + """ + Get the skip headers limit value + Returns: + int: Skip headers limit, or 0 if not set + """ + return self.get_file_snippet_settings().get('skip_headers_limit', 0) + + def get_skip_headers(self) -> bool: + """ + Get whether to skip headers + Returns: + bool: True to skip headers, False otherwise (default) + """ + return self.get_file_snippet_settings().get('skip_headers', False) + + def get_proxy(self) -> Optional[dict]: + """ + Get the root-level proxy configuration + Returns: + dict or None: Proxy configuration with 'host' key, or None if not set + """ + return self.data.get('settings', {}).get('proxy') + + def get_http_config(self) -> Optional[dict]: + """ + Get the root-level http_config configuration + Returns: + dict or None: HTTP config with 'base_uri' and 'ignore_cert_errors' keys, or None if not set + """ + return self.data.get('settings', {}).get('http_config') + + def get_file_snippet_proxy(self) -> Optional[dict]: + """ + Get the file_snippet-level proxy configuration (takes priority over root) + Returns: + dict or None: Proxy configuration with 'host' key, or None if not set + """ + return self.get_file_snippet_settings().get('proxy') + + def get_file_snippet_http_config(self) -> Optional[dict]: + """ + Get the file_snippet-level http_config configuration (takes priority over root) + Returns: + dict or None: HTTP config with 'base_uri' and 'ignore_cert_errors' keys, or None if not set + """ + return self.get_file_snippet_settings().get('http_config') diff --git a/src/scanoss/scanossapi.py b/src/scanoss/scanossapi.py index 65fea09b..a6816e88 100644 --- a/src/scanoss/scanossapi.py +++ b/src/scanoss/scanossapi.py @@ -1,40 +1,54 @@ """ - SPDX-License-Identifier: MIT - - Copyright (c) 2021, SCANOSS - - 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. +SPDX-License-Identifier: MIT + + Copyright (c) 2022, SCANOSS + + 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. """ + +import base64 +import http.client as http_client +import json import logging import os import sys import time +import uuid from json.decoder import JSONDecodeError +from typing import Optional, Union +from urllib.parse import urlparse, urlunparse + import requests -import uuid -import http.client as http_client +import urllib3 +from pypac import PACSession +from pypac.parser import PACFile +from urllib3.exceptions import InsecureRequestWarning +from . import __version__ +from .constants import DEFAULT_TIMEOUT, MIN_TIMEOUT from .scanossbase import ScanossBase -DEFAULT_URL = "https://osskb.org/api/scan/direct" -SCANOSS_SCAN_URL = os.environ.get("SCANOSS_SCAN_URL") if os.environ.get("SCANOSS_SCAN_URL") else DEFAULT_URL -SCANOSS_API_KEY = os.environ.get("SCANOSS_API_KEY") if os.environ.get("SCANOSS_API_KEY") else '' +DEFAULT_URL = 'https://api.osskb.org' # default free service base URL +DEFAULT_URL2 = 'https://api.scanoss.com' # default premium service base URL +SCAN_ENDPOINT = '/scan/direct' # scan endpoint path +SCANOSS_SCAN_URL = os.environ.get('SCANOSS_SCAN_URL') if os.environ.get('SCANOSS_SCAN_URL') else DEFAULT_URL +SCANOSS_API_KEY = os.environ.get('SCANOSS_API_KEY') if os.environ.get('SCANOSS_API_KEY') else '' class ScanossApi(ScanossBase): @@ -43,124 +57,316 @@ class ScanossApi(ScanossBase): Currently support posting scan requests to the SCANOSS streaming API """ - def __init__(self, scan_type: str = None, sbom_path: str = None, scan_format: str = None, flags: str = None, - url: str = None, api_key: str = None, debug: bool = False, trace: bool = False, quiet: bool = False, - timeout: int = 120): + def normalize_api_url(self, url: str) -> str: + """ + Normalize API URL to ensure it's a base URL with the scan endpoint appended. + + If the URL contains a path component (e.g., /scan/direct), a warning is emitted + and the path is stripped to use only the base URL. + + :param url: Input URL (can be base URL or full endpoint URL) + :return: Normalized URL with /scan/direct endpoint + """ + if not url: + return url + + url = url.strip() + parsed = urlparse(url) + + if parsed.path and parsed.path != '/': + self.print_stderr( + f"Warning: URL '{url}' contains path '{parsed.path}'. " + f"Using base URL only: '{parsed.scheme}://{parsed.netloc}'" + ) + base_url = urlunparse((parsed.scheme, parsed.netloc, '', '', '', '')) + else: + base_url = url.rstrip('/') + + return f'{base_url}{SCAN_ENDPOINT}' + + def __init__( # noqa: PLR0912, PLR0913, PLR0915 + self, + scan_format: str = None, + flags: str = None, + url: str = None, + api_key: str = None, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + timeout: int = DEFAULT_TIMEOUT, + ver_details: str = None, + ignore_cert_errors: bool = False, + proxy: str = None, + ca_cert: str = None, + pac: PACFile = None, + retry: int = 5, + req_headers: dict = None, + min_snippet_hits: int = None, + min_snippet_lines: int = None, + honour_file_exts: Union[bool, str, None] = 'unset', + ranking: Union[bool, str, None] = 'unset', + ranking_threshold: int = None, + ): """ Initialise the SCANOSS API - :param scan_type: Scan type (default identify) - :param sbom_path: Input SBOM file to match scan type (default None) :param scan_format: Scan format (default plain) :param flags: Scanning flags (default None) - :param url: API URL (default https://osskb.org/api/scan/direct) + :param url: API base URL (default https://api.osskb.org). The /scan/direct endpoint is automatically appended. :param api_key: API Key (default None) :param debug: Enable debug (default False) :param trace: Enable trace (default False) - :param quiet: Enable quite mode (default False) + :param quiet: Enable quiet mode (default False) + :param min_snippet_hits: Minimum snippet hits required (default None) + :param min_snippet_lines: Minimum snippet lines required (default None) + :param honour_file_exts: Whether to honour file extensions (default 'unset') + :param ranking: Enable/disable ranking (default 'unset') + :param ranking_threshold: Ranking threshold value (default None) + + To set a custom certificate use: + REQUESTS_CA_BUNDLE=/path/to/cert.pem + To enable a Proxy use: + HTTP_PROXY='http://:' + HTTPS_PROXY='http://:' """ super().__init__(debug, trace, quiet) - self.url = url if url else SCANOSS_SCAN_URL - self.api_key = api_key - self.scan_type = scan_type - self.sbom_path = sbom_path + # Scan tuning parameters + self.min_snippet_hits = min_snippet_hits + self.min_snippet_lines = min_snippet_lines + self.honour_file_exts = honour_file_exts + self.ranking = ranking + self.ranking_threshold = ranking_threshold self.scan_format = scan_format if scan_format else 'plain' self.flags = flags - self.timeout = timeout if timeout > 5 else 120 + self.timeout = timeout if timeout > MIN_TIMEOUT else DEFAULT_TIMEOUT + self.retry_limit = retry if retry >= 0 else 5 + self.ignore_cert_errors = ignore_cert_errors + self.req_headers = req_headers if req_headers else {} self.headers = {} + base_url = url if url else SCANOSS_SCAN_URL + self.api_key = api_key if api_key else SCANOSS_API_KEY + if self.api_key and not url and not os.environ.get('SCANOSS_SCAN_URL'): + base_url = DEFAULT_URL2 + self.url = self.normalize_api_url(base_url) + if ver_details: + self.headers['x-scanoss-client'] = ver_details if self.api_key: self.headers['X-Session'] = self.api_key - self.sbom = None - self.load_sbom() # Load an input SBOM if one is specified + self.headers['x-api-key'] = self.api_key + user_agent = f'scanoss-py/{__version__}' + self.headers['User-Agent'] = user_agent + self.headers['user-agent'] = user_agent + self.load_generic_headers(url) + if self.trace: logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) http_client.HTTPConnection.debuglevel = 1 + if pac and not proxy: + self.print_debug('Setting up PAC session...') + self.session = PACSession(pac=pac) + else: + self.session = requests.sessions.Session() + self.verify = None + if self.ignore_cert_errors: + self.print_debug('Ignoring cert errors...') + urllib3.disable_warnings(InsecureRequestWarning) + self.verify = False + self.session.verify = False + elif ca_cert: + self.verify = ca_cert + self.session.verify = ca_cert + self.proxies = {'https': proxy, 'http': proxy} if proxy else None + if self.proxies: + self.session.proxies = self.proxies - def load_sbom(self): - """ - Load the input SBOM if one exists - """ - if self.sbom_path: - if not self.scan_type: - self.scan_type = 'identify' # Default to identify SBOM type if it's not set - self.print_debug(f'Loading {self.scan_type} SBOM {self.sbom_path}...') - with open(self.sbom_path) as f: - self.sbom = f.read() - - def scan(self, wfp: str, context: str = None, scan_id: int = None): + def scan(self, wfp: str, context: str = None, scan_id: int = None, sbom: dict = None): # noqa: PLR0912, PLR0915 """ Scan the specified WFP and return the JSON object :param wfp: WFP to scan :param context: Context to help with identification :param scan_id: ID of the scan being run (usually thread id) + :param sbom: Per-request SBOM context :return: JSON result object """ + request_id = str(uuid.uuid4()) form_data = {} - if self.sbom: - form_data['type'] = self.scan_type - form_data['assets'] = self.sbom + if sbom: + form_data['type'] = sbom.get('scan_type') + form_data['assets'] = sbom.get('assets') if self.scan_format: form_data['format'] = self.scan_format if self.flags: form_data['flags'] = self.flags if context: form_data['context'] = context - scan_files = {'file': ("%s.wfp" % uuid.uuid1().hex, wfp)} + + scan_files = {'file': ('%s.wfp' % request_id, wfp)} + headers = self.headers + headers['x-request-id'] = request_id # send a unique request id for each post + # Add scan settings header if any settings are configured + scan_settings_header = self._build_scan_settings_header() + if scan_settings_header: + headers['scanoss-settings'] = scan_settings_header r = None retry = 0 # Add some retry logic to cater for timeouts, etc. - while retry <= 5: + while retry <= self.retry_limit: retry += 1 try: r = None - r = requests.post(self.url, files=scan_files, data=form_data, headers=self.headers, - timeout=self.timeout) + r = self.session.post( + self.url, files=scan_files, data=form_data, headers=self.headers, timeout=self.timeout + ) + except (requests.exceptions.SSLError, requests.exceptions.ProxyError) as e: + self.print_stderr(f'ERROR: Exception ({e.__class__.__name__}) POSTing data - {e}.') + raise Exception(f'ERROR: The SCANOSS API request failed for {self.url}') from e except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e: - if retry > 5: # Timed out 5 or more times, fail - self.print_stderr(f'ERROR: Timeout/Connection Error POSTing data: {scan_files}') - raise Exception(f"ERROR: The SCANOSS API request timed out for {self.url}") from e + if retry > self.retry_limit: # Timed out retry_limit or more times, fail + self.print_stderr(f'ERROR: {e.__class__.__name__} POSTing data ({request_id}) - {e}: {scan_files}') + raise Exception( + f'ERROR: The SCANOSS API request timed out ({e.__class__.__name__}) for {self.url}' + ) from e else: - self.print_stderr(f'Warning: Timeout/Connection Error communicating with {self.url}. Retrying...') + self.print_stderr(f'Warning: {e.__class__.__name__} communicating with {self.url}. Retrying...') time.sleep(5) except Exception as e: - self.print_stderr(f'ERROR: Exception POSTing data: {scan_files}') - raise Exception(f"ERROR: The SCANOSS API request failed for {self.url}") from e + self.print_stderr( + f'ERROR: Exception ({e.__class__.__name__}) POSTing data ({request_id}) - {e}: {scan_files}' + ) + raise Exception(f'ERROR: The SCANOSS API request failed for {self.url}') from e else: - if not r: - if retry > 5: # No response 5 or more times, fail - raise Exception(f"ERROR: The SCANOSS API request response object is empty for {self.url}") + if r is None: + if retry > self.retry_limit: # No response retry_limit or more times, fail + self.save_bad_req_wfp(scan_files, request_id, scan_id) + raise Exception( + f'ERROR: The SCANOSS API request ({request_id}) response object is empty for {self.url}' + ) else: self.print_stderr(f'Warning: No response received from {self.url}. Retrying...') time.sleep(5) - elif r.status_code >= 400: - if retry > 5: # No response 5 or more times, fail + elif r.status_code == requests.codes.service_unavailable: # Service limits most likely reached + self.print_stderr( + f'ERROR: SCANOSS API rejected the scan request ({request_id}) due to ' + f'service limits being exceeded' + ) + self.print_stderr(f'ERROR: Details: {r.text.strip()}') + raise Exception( + f'ERROR: {r.status_code} - The SCANOSS API request ({request_id}) rejected ' + f'for {self.url} due to service limits being exceeded.' + ) + elif r.status_code >= requests.codes.bad_request: + if retry > self.retry_limit: # No response retry_limit or more times, fail + self.save_bad_req_wfp(scan_files, request_id, scan_id) raise Exception( - f"ERROR: The SCANOSS API returned the following error: HTTP {r.status_code}, {r.text}") + f'ERROR: The SCANOSS API returned the following error: HTTP {r.status_code}, ' + f'{r.text.strip()}' + ) else: - self.print_stderr(f'Warning: Error response code {r.status_code} from {self.url}. Retrying...') + self.save_bad_req_wfp(scan_files, request_id, scan_id) + self.print_stderr( + f'Warning: Error response code {r.status_code} ({r.text.strip()}) from ' + f'{self.url}. Retrying...' + ) time.sleep(5) else: break # Valid response, break out of the retry loop # End of while loop - if not r: - raise Exception(f"ERROR: The SCANOSS API request response object is empty for {self.url}") + if r is None: + self.save_bad_req_wfp(scan_files, request_id, scan_id) + raise Exception(f'ERROR: The SCANOSS API request response object is empty for {self.url}') try: - if 'xml' in self.scan_format: + if 'xml' in self.scan_format: # TODO remove XML parsing option? return r.text json_resp = r.json() return json_resp except (JSONDecodeError, Exception) as e: - self.print_stderr(f'ERROR: The SCANOSS API returned an invalid JSON: {e}') - ctime = int(time.time()) - bad_json_file = f'bad_json-{scan_id}-{ctime}.txt' if scan_id else f'bad_json-{ctime}.txt' + self.print_stderr( + f'ERROR: The SCANOSS API returned an invalid JSON ({e.__class__.__name__} - {request_id}): {e}' + ) + bad_json_file = f'bad_json-{scan_id}-{request_id}.txt' if scan_id else f'bad_json-{request_id}.txt' self.print_stderr(f'Ignoring result. Please look in "{bad_json_file}" for more details.') try: with open(bad_json_file, 'w') as f: - f.write(f"---WFP Begin---\n{scan_files}\n---WFP End---\n---Bad JSON Begin---\n") + f.write(f'---Request ID Begin---\n{request_id}\n---Request ID End---\n') + f.write(f'---WFP Begin---\n{scan_files}\n---WFP End---\n---Bad JSON Begin---\n') f.write(r.text) - f.write("---Bad JSON End---\n") + f.write('---Bad JSON End---\n') except Exception as ee: - self.print_stderr(f'Warning: Issue writing bad json file - {bad_json_file}: {ee}') + self.print_stderr( + f'Warning: Issue writing bad json file - {bad_json_file} ({ee.__class__.__name__}): {ee}' + ) return None + def save_bad_req_wfp(self, scan_files, request_id, scan_id): + """ + Save the given WFP to a bad_request file + :param scan_files: WFP + :param request_id: request ID + :param scan_id: scan thread id (optional) + """ + bad_req_file = f'bad_request-{scan_id}-{request_id}.txt' if scan_id else f'bad_request-{request_id}.txt' + try: + self.print_stderr( + f'No response object returned from API. Please look in "{bad_req_file}" for the offending WFP.' + ) + with open(bad_req_file, 'w') as f: + f.write(f'---Request ID Begin---\n{request_id}\n---Request ID End---\n') + f.write(f'---WFP Begin---\n{scan_files}\n---WFP End---\n') + except Exception as ee: + self.print_stderr( + f'Warning: Issue writing bad request file - {bad_req_file} ({ee.__class__.__name__}): {ee}' + ) + + def _build_scan_settings_header(self) -> Optional[str]: + """ + Build base64-encoded JSON for x-scanoss-scan-settings header. + Only includes parameters that have meaningful (non-"unset") values. + Returns: + Base64-encoded JSON string, or None if no settings to send + """ + settings = {} + + # min_snippet_hits: 0 = unset, don't send + if self.min_snippet_hits is not None and self.min_snippet_hits != 0: + settings['min_snippet_hits'] = self.min_snippet_hits + + # min_snippet_lines: 0 = unset, don't send + if self.min_snippet_lines is not None and self.min_snippet_lines != 0: + settings['min_snippet_lines'] = self.min_snippet_lines + + # honour_file_exts: None = unset, don't send + if self.honour_file_exts is not None and self.honour_file_exts != 'unset': + settings['honour_file_exts'] = self.honour_file_exts + + # ranking: None = unset, don't send + if self.ranking is not None and self.ranking != 'unset': + settings['ranking_enabled'] = self.ranking + + # ranking_threshold: -1 = unset, don't send + if self.ranking_threshold is not None and self.ranking_threshold != -1: + settings['ranking_threshold'] = self.ranking_threshold + + if settings: + json_str = json.dumps(settings) + self.print_debug(f'Scan settings: {json_str}') + return base64.b64encode(json_str.encode()).decode() + return None + + def load_generic_headers(self, url): + """ + Adds custom headers from req_headers to the headers collection. + + If x-api-key is present and no URL is configured (directly or via + environment), sets URL to the premium endpoint (DEFAULT_URL2). + """ + if self.req_headers: # Load generic headers + for key, value in self.req_headers.items(): + if key == 'x-api-key': # Set premium URL if x-api-key header is set + if not url and not os.environ.get('SCANOSS_SCAN_URL'): + # API key specific and no alternative URL, so use the default premium + self.url = self.normalize_api_url(DEFAULT_URL2) + self.api_key = value + self.headers[key] = value + + # # End of ScanossApi Class # diff --git a/src/scanoss/scanossbase.py b/src/scanoss/scanossbase.py index b4941617..07409af0 100644 --- a/src/scanoss/scanossbase.py +++ b/src/scanoss/scanossbase.py @@ -1,25 +1,25 @@ """ - SPDX-License-Identifier: MIT - - Copyright (c) 2021, SCANOSS - - 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. +SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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. """ import sys @@ -50,7 +50,7 @@ def print_stderr(*args, **kwargs): def print_msg(self, *args, **kwargs): """ - Print message if quite mode is not enabled + Print message if quiet mode is not enabled """ if not self.quiet: self.print_stderr(*args, **kwargs) @@ -68,3 +68,40 @@ def print_trace(self, *args, **kwargs): """ if self.trace: self.print_stderr(*args, **kwargs) + + @staticmethod + def print_stdout(*args, **kwargs): + """ + Print message to STDOUT + """ + print( + *args, + file=sys.stdout, + **kwargs, + ) + + def print_to_file_or_stdout(self, content: str, file: str = None): + """ + Print message to file if provided or stdout + """ + if not content: + return + + if file: + with open(file, 'w') as f: + f.write(content) + else: + self.print_stdout(content) + + def print_to_file_or_stderr(self, msg: str, file: str = None): + """ + Print message to file if provided or stderr + """ + if not msg: + return + + if file: + with open(file, 'w') as f: + f.write(msg) + else: + self.print_stderr(msg) diff --git a/src/scanoss/scanossgrpc.py b/src/scanoss/scanossgrpc.py index 096839ec..1eab55e0 100644 --- a/src/scanoss/scanossgrpc.py +++ b/src/scanoss/scanossgrpc.py @@ -1,42 +1,138 @@ """ - SPDX-License-Identifier: MIT - - Copyright (c) 2021, SCANOSS - - 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. +SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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. """ +import concurrent.futures +import http.client as http_client +import logging import os -import grpc -import json +import sys +import time +import uuid +from dataclasses import dataclass +from enum import Enum, IntEnum +from typing import Dict, Optional from urllib.parse import urlparse + +import grpc +import requests +import urllib3 from google.protobuf.json_format import MessageToDict, ParseDict +from pypac import PACSession +from pypac.parser import PACFile +from pypac.resolver import ProxyResolver +from urllib3.exceptions import InsecureRequestWarning + +from scanoss.api.licenses.v2.scanoss_licenses_pb2_grpc import LicenseStub +from scanoss.api.scanning.v2.scanoss_scanning_pb2_grpc import ScanningStub +from scanoss.constants import DEFAULT_TIMEOUT +from . import __version__ +from .api.common.v2.scanoss_common_pb2 import ( + ComponentsRequest, + EchoRequest, + StatusCode, + StatusResponse, +) +from .api.components.v2.scanoss_components_pb2 import ( + CompSearchRequest, + CompVersionRequest, +) +from .api.components.v2.scanoss_components_pb2_grpc import ComponentsStub +from .api.cryptography.v2.scanoss_cryptography_pb2_grpc import CryptographyStub +from .api.dependencies.v2.scanoss_dependencies_pb2 import DependencyRequest from .api.dependencies.v2.scanoss_dependencies_pb2_grpc import DependenciesStub -from .api.dependencies.v2.scanoss_dependencies_pb2 import DependencyRequest, DependencyResponse -from .api.common.v2.scanoss_common_pb2 import EchoRequest, EchoResponse, StatusResponse, StatusCode +from .api.geoprovenance.v2.scanoss_geoprovenance_pb2_grpc import GeoProvenanceStub +from .api.scanning.v2.scanoss_scanning_pb2 import HFHRequest +from .api.semgrep.v2.scanoss_semgrep_pb2_grpc import SemgrepStub +from .api.vulnerabilities.v2.scanoss_vulnerabilities_pb2_grpc import VulnerabilitiesStub from .scanossbase import ScanossBase -# DEFAULT_URL = "https://osskb.org" -# DEFAULT_URL = "localhost:50051" -DEFAULT_URL = "https://scanoss.com" -SCANOSS_GRPC_URL = os.environ.get("SCANOSS_GRPC_URL") if os.environ.get("SCANOSS_GRPC_URL") else DEFAULT_URL +DEFAULT_URL = 'https://api.osskb.org' # default free service URL +DEFAULT_URL2 = 'https://api.scanoss.com' # default premium service URL +SCANOSS_GRPC_URL = os.environ.get('SCANOSS_GRPC_URL') if os.environ.get('SCANOSS_GRPC_URL') else DEFAULT_URL +SCANOSS_API_KEY = os.environ.get('SCANOSS_API_KEY') if os.environ.get('SCANOSS_API_KEY') else '' +DEFAULT_URI_PREFIX = '/v2' + +MAX_CONCURRENT_REQUESTS = 5 # Maximum number of concurrent requests to make + +# REST API endpoint mappings with HTTP methods +REST_ENDPOINTS = { + 'vulnerabilities.GetComponentsVulnerabilities': {'path': '/vulnerabilities/components', 'method': 'POST'}, + 'dependencies.Echo': {'path': '/dependencies/echo', 'method': 'POST'}, + 'dependencies.GetDependencies': {'path': '/dependencies/dependencies', 'method': 'POST'}, + 'cryptography.Echo': {'path': '/cryptography/echo', 'method': 'POST'}, + 'cryptography.GetComponentsAlgorithms': {'path': '/cryptography/algorithms/components', 'method': 'POST'}, + 'cryptography.GetComponentsAlgorithmsInRange': { + 'path': '/cryptography/algorithms/range/components', + 'method': 'POST', + }, + 'cryptography.GetComponentsEncryptionHints': {'path': '/cryptography/hints/components', 'method': 'POST'}, + 'cryptography.GetComponentsHintsInRange': {'path': '/cryptography/hints/components/range', 'method': 'POST'}, + 'cryptography.GetComponentsVersionsInRange': { + 'path': '/cryptography/algorithms/versions/range/components', + 'method': 'POST', + }, + 'components.SearchComponents': {'path': '/components/search', 'method': 'GET'}, + 'components.GetComponentVersions': {'path': '/components/versions', 'method': 'GET'}, + 'components.GetComponentsStatus': {'path': '/components/status/components', 'method': 'POST'}, + 'geoprovenance.GetCountryContributorsByComponents': { + 'path': '/geoprovenance/countries/components', + 'method': 'POST', + }, + 'geoprovenance.GetOriginByComponents': {'path': '/geoprovenance/origin/components', 'method': 'POST'}, + 'licenses.GetComponentsLicenses': {'path': '/licenses/components', 'method': 'POST'}, + 'semgrep.GetComponentsIssues': {'path': '/semgrep/issues/components', 'method': 'POST'}, + 'scanning.FolderHashScan': {'path': '/scanning/hfh/scan', 'method': 'POST'}, +} + + +class ScanossGrpcError(Exception): + """ + Custom exception for SCANOSS gRPC errors + """ + + pass + + +class ScanossGrpcStatusCode(IntEnum): + """Status codes for SCANOSS gRPC responses""" + + UNSPECIFIED = 0 + SUCCESS = 1 + SUCCEEDED_WITH_WARNINGS = 2 + WARNING = 3 + FAILED = 4 + + +class ScanossRESTStatusCode(Enum): + """Status codes for SCANOSS REST responses""" + + UNSPECIFIED = 'UNSPECIFIED' + SUCCESS = 'SUCCESS' + SUCCEEDED_WITH_WARNINGS = 'SUCCEEDED_WITH_WARNINGS' + WARNING = 'WARNING' + FAILED = 'FAILED' class ScanossGrpc(ScanossBase): @@ -44,19 +140,89 @@ class ScanossGrpc(ScanossBase): Client for gRPC functionality """ - def __init__(self, url: str = None, debug: bool = False, trace: bool = False, quiet: bool = False, - cert: bytes = None): + def __init__( # noqa: PLR0912, PLR0913, PLR0915 + self, + url: Optional[str] = None, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + ca_cert: Optional[str] = None, + api_key: Optional[str] = None, + ver_details: Optional[str] = None, + timeout: int = 600, + proxy: Optional[str] = None, + grpc_proxy: Optional[str] = None, + pac: Optional[PACFile] = None, + req_headers: Optional[dict] = None, + ignore_cert_errors: bool = False, + use_grpc: Optional[bool] = False, + ): """ :param url: :param debug: :param trace: :param quiet: - :param cert: + :param ca_cert: + + To set a custom certificate use: + GRPC_DEFAULT_SSL_ROOTS_FILE_PATH=/path/to/certs/cert.pem + More details here: https://grpc.github.io/grpc/cpp/grpc__security__constants_8h.html + https://github.com/grpc/grpc/blob/master/doc/environment_variables.md + To enable a Proxy use: + grpc_proxy='http://:' """ super().__init__(debug, trace, quiet) + self.api_key = api_key if api_key else SCANOSS_API_KEY + self.timeout = timeout + self.proxy = proxy + self.grpc_proxy = grpc_proxy + self.pac = pac + self.metadata = [] + self.ignore_cert_errors = ignore_cert_errors + self.use_grpc = use_grpc + self.req_headers = req_headers if req_headers else {} + self.headers = {} + self.retry_limit = 2 # default retry limit + + if self.api_key: + self.metadata.append(('x-api-key', api_key)) # Set API key if we have one + self.headers['X-Session'] = self.api_key + self.headers['x-api-key'] = self.api_key + if ver_details: + self.metadata.append(('x-scanoss-client', ver_details)) + self.headers['x-scanoss-client'] = ver_details + user_agent = f'scanoss-py/{__version__}' + self.metadata.append(('user-agent', user_agent)) + self.headers['User-Agent'] = user_agent + self.headers['user-agent'] = user_agent + self.headers['Content-Type'] = 'application/json' + # Set the correct URL/API key combination self.url = url if url else SCANOSS_GRPC_URL + if self.api_key and not url and not os.environ.get('SCANOSS_GRPC_URL'): + self.url = DEFAULT_URL2 # API key specific and no alternative URL, so use the default premium + self.load_generic_headers(url) self.url = self.url.lower() + self.orig_url = self.url.strip().rstrip('/') # Used for proxy lookup + # REST setup + if self.trace: + logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) + http_client.HTTPConnection.debuglevel = 1 + if pac and not proxy: # Set up a PAC session if requested (and no proxy has been explicitly set) + self.print_debug('Setting up PAC session...') + self.session = PACSession(pac=pac) + else: + self.session = requests.sessions.Session() + if self.ignore_cert_errors: + self.print_debug('Ignoring cert errors...') + urllib3.disable_warnings(InsecureRequestWarning) + self.session.verify = False + elif ca_cert: + self.session.verify = ca_cert + self.proxies = {'https': proxy, 'http': proxy} if proxy else None + if self.proxies: + self.session.proxies = self.proxies + secure = True if self.url.startswith('https:') else False # Is it a secure connection? if self.url.startswith('http'): u = urlparse(self.url) @@ -64,36 +230,59 @@ def __init__(self, url: str = None, debug: bool = False, trace: bool = False, qu if port is None: port = 443 if u.scheme == 'https' else 80 # Set the default port number if it's not available self.url = f'{u.hostname}:{port}' - if cert is not None: + cert_data = None + if ca_cert is not None: secure = True + cert_data = ScanossGrpc._load_cert(ca_cert) self.print_debug(f'Setting up (secure: {secure}) connection to {self.url}...') - if secure is False: - self.dependencies_stub = DependenciesStub(grpc.insecure_channel(self.url)) # insecure connection + self._get_proxy_config() + if not secure: # insecure connection + self.comp_search_stub = ComponentsStub(grpc.insecure_channel(self.url)) + self.crypto_stub = CryptographyStub(grpc.insecure_channel(self.url)) + self.dependencies_stub = DependenciesStub(grpc.insecure_channel(self.url)) + self.semgrep_stub = SemgrepStub(grpc.insecure_channel(self.url)) + self.vuln_stub = VulnerabilitiesStub(grpc.insecure_channel(self.url)) + self.provenance_stub = GeoProvenanceStub(grpc.insecure_channel(self.url)) + self.scanning_stub = ScanningStub(grpc.insecure_channel(self.url)) + self.license_stub = LicenseStub(grpc.insecure_channel(self.url)) else: - if cert is not None: - credentials = grpc.ssl_channel_credentials(cert) # secure with specified certificate + if ca_cert is not None: + credentials = grpc.ssl_channel_credentials(cert_data) # secure with specified certificate else: - credentials = grpc.ssl_channel_credentials() # secure connection with default certificate + credentials = grpc.ssl_channel_credentials() # secure connection with default certificate + self.comp_search_stub = ComponentsStub(grpc.secure_channel(self.url, credentials)) + self.crypto_stub = CryptographyStub(grpc.secure_channel(self.url, credentials)) self.dependencies_stub = DependenciesStub(grpc.secure_channel(self.url, credentials)) + self.semgrep_stub = SemgrepStub(grpc.secure_channel(self.url, credentials)) + self.vuln_stub = VulnerabilitiesStub(grpc.secure_channel(self.url, credentials)) + self.provenance_stub = GeoProvenanceStub(grpc.secure_channel(self.url, credentials)) + self.scanning_stub = ScanningStub(grpc.secure_channel(self.url, credentials)) + self.license_stub = LicenseStub(grpc.secure_channel(self.url, credentials)) + + @classmethod + def _load_cert(cls, cert_file: str) -> bytes: + with open(cert_file, 'rb') as f: + return f.read() - def deps_echo(self, message: str = 'Hello there!') -> str: + def deps_echo(self, message: str = 'Hello there!') -> Optional[dict]: """ Send Echo message to the Dependency service + :param self: :param message: Message to send (default: Hello there!) :return: echo or None """ - resp: EchoResponse - try: - resp = self.dependencies_stub.Echo(EchoRequest(message=message)) - except Exception as e: - self.print_stderr(f'ERROR: Problem encountered sending gRPC message: {e}') - else: - if resp: - return resp.message - self.print_stderr(f'ERROR: Problem sending Echo request ({message}) to {self.url}') - return None + return self._call_api('dependencies.Echo', self.dependencies_stub.Echo, {'message': message}, EchoRequest) + + def crypto_echo(self, message: str = 'Hello there!') -> Optional[dict]: + """ + Send Echo message to the Cryptography service + :param self: + :param message: Message to send (default: Hello there!) + :return: echo or None + """ + return self._call_api('cryptography.Echo', self.crypto_stub.Echo, {'message': message}, EchoRequest) - def get_dependencies(self, dependencies: json, depth: int = 1) -> dict: + def get_dependencies(self, dependencies: Optional[dict] = None, depth: int = 1) -> Optional[dict]: if not dependencies: self.print_stderr('ERROR: No dependency data supplied to submit to the API.') return None @@ -102,7 +291,7 @@ def get_dependencies(self, dependencies: json, depth: int = 1) -> dict: self.print_stderr(f'ERROR: No response for dependency request: {dependencies}') return resp - def get_dependencies_json(self, dependencies: dict, depth: int = 1) -> dict: + def get_dependencies_json(self, dependencies: dict, depth: int = 1) -> Optional[dict]: """ Client function to call the rpc for GetDependencies :param dependencies: Message to send to the service @@ -110,68 +299,693 @@ def get_dependencies_json(self, dependencies: dict, depth: int = 1) -> dict: :return: Server response or None """ if not dependencies: - self.print_stderr(f'ERROR: No message supplied to send to gRPC service.') + self.print_stderr('ERROR: No message supplied to send to gRPC service.') return None - resp: DependencyResponse - try: - files_json = dependencies.get("files") - if files_json is None or len(files_json) == 0: - self.print_stderr(f'ERROR: No dependency data supplied to send to gRPC service.') - return None - request = ParseDict(dependencies, DependencyRequest()) # Parse the JSON/Dict into the dependency object - request.depth = depth - resp = self.dependencies_stub.GetDependencies(request) - except Exception as e: - self.print_stderr(f'ERROR: Problem encountered sending gRPC message: {e}') - else: - if resp: - if not self._check_status_response(resp.status): - return None - return MessageToDict(resp, preserving_proto_field_name=True) # Convert the gRPC response to a dictionary - return None + files_json = dependencies.get('files') + if files_json is None or len(files_json) == 0: + self.print_stderr('ERROR: No dependency data supplied to send to decoration service.') + return None + all_responses = [] + # Process the dependency files in parallel + with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_CONCURRENT_REQUESTS) as executor: + future_to_file = { + executor.submit(self._process_dep_file, file, depth, self.use_grpc): file for file in files_json + } + for future in concurrent.futures.as_completed(future_to_file): + response = future.result() + if response: + all_responses.append(response) + # End of concurrent processing + success_status = 'SUCCESS' + merged_response = {'files': [], 'status': {'status': success_status, 'message': 'Success'}} + # Merge the responses + for response in all_responses: + if response: + if 'files' in response and len(response['files']) > 0: + merged_response['files'].append(response['files'][0]) + # Overwrite the status if any of the responses was not successful + if 'status' in response and response['status']['status'] != success_status: + merged_response['status'] = response['status'] + return merged_response - def get_dependencies_str(self, dependencies: str, depth: int = 1) -> str: # TODO remove? + def _process_dep_file(self, file, depth: int = 1, use_grpc: Optional[bool] = None) -> Optional[dict]: """ - Client function to call the rpc for GetDependencies - :param dependencies: Message to send to the service - :param depth: depth of sub-dependencies to search (default: 1) - :return: Server response or None + Process a single dependency file using either gRPC or REST + + Args: + file: dependency file purls + depth: depth to search (default: 1) + use_grpc: Whether to use gRPC or REST (None = use instance default) + + Returns: + response JSON or None """ - if not dependencies: - self.print_stderr(f'ERROR: No message supplied to send to gRPC service.') + file_request = {'files': [file], 'depth': depth} + + return self._call_api( + 'dependencies.GetDependencies', + self.dependencies_stub.GetDependencies, + file_request, + DependencyRequest, + 'Sending dependency data for decoration (rqId: {rqId})...', + use_grpc=use_grpc, + ) + + def get_vulnerabilities_json(self, purls: Optional[dict] = None, use_grpc: Optional[bool] = None) -> Optional[dict]: + """ + Client function to call the rpc for Vulnerability GetVulnerabilities + It will either use REST (default) or gRPC + + Args: + purls (dict): Message to send to the service + + Returns: + Server response or None + """ + return self._call_api( + 'vulnerabilities.GetComponentsVulnerabilities', + self.vuln_stub.GetComponentsVulnerabilities, + purls, + ComponentsRequest, + 'Sending vulnerability data for decoration (rqId: {rqId})...', + use_grpc=use_grpc, + ) + + def get_semgrep_json(self, purls: Optional[dict] = None, use_grpc: Optional[bool] = None) -> Optional[dict]: + """ + Client function to call the rpc for Semgrep GetIssues + + Args: + purls (dict): Message to send to the service + use_grpc (bool): Whether to use gRPC or REST + + Returns: + Server response or None + """ + return self._call_api( + 'semgrep.GetComponentsIssues', + self.semgrep_stub.GetComponentsIssues, + purls, + ComponentsRequest, + 'Sending semgrep data for decoration (rqId: {rqId})...', + use_grpc=use_grpc, + ) + + def search_components_json(self, search: dict, use_grpc: Optional[bool] = None) -> Optional[dict]: + """ + Client function to call the rpc for Components SearchComponents + + Args: + search (dict): Message to send to the service + Returns: + Server response or None + """ + return self._call_api( + 'components.SearchComponents', + self.comp_search_stub.SearchComponents, + search, + CompSearchRequest, + 'Sending component search data for decoration (rqId: {rqId})...', + use_grpc=use_grpc, + ) + + def get_component_versions_json(self, search: dict, use_grpc: Optional[bool] = None) -> Optional[dict]: + """ + Client function to call the rpc for Components GetComponentVersions + + Args: + search (dict): Message to send to the service + Returns: + Server response or None + """ + return self._call_api( + 'components.GetComponentVersions', + self.comp_search_stub.GetComponentVersions, + search, + CompVersionRequest, + 'Sending component version data for decoration (rqId: {rqId})...', + use_grpc=use_grpc, + ) + + def folder_hash_scan(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]: + """ + Client function to call the rpc for Folder Hashing Scan + + Args: + request (Dict): Folder Hash Request + use_grpc (Optional[bool]): Whether to use gRPC or REST API + + Returns: + Optional[Dict]: Folder Hash Response, or None if the request was not succesfull + """ + return self._call_api( + 'scanning.FolderHashScan', + self.scanning_stub.FolderHashScan, + request, + HFHRequest, + 'Sending folder hash scan data (rqId: {rqId})...', + use_grpc=use_grpc, + ) + + def _call_api( + self, + endpoint_key: str, + rpc_method, + request_input, + request_type, + debug_msg: Optional[str] = None, + use_grpc: Optional[bool] = None, + ) -> Optional[Dict]: + """ + Unified method to call either gRPC or REST API based on configuration + + Args: + endpoint_key (str): The key to lookup the REST endpoint in REST_ENDPOINTS + rpc_method: The gRPC stub method (used only if use_grpc is True) + request_input: Either a dict or a gRPC request object + request_type: The type of the gRPC request object (used only if use_grpc is True) + debug_msg (str, optional): Debug message template that can include {rqId} placeholder + use_grpc (bool, optional): Override the instance's use_grpc setting. If None, uses self.use_grpc + + Returns: + dict: The parsed response as a dictionary, or None if something went wrong + """ + if not request_input: + self.print_stderr('ERROR: No message supplied to send to service.') return None - resp: DependencyResponse - try: - purl = DependencyRequest.Purls(purl="pkg", requirement="^1.0") - purls = [purl] - dep_req = DependencyRequest.Files(file="package.json", purls=purls) - files = [dep_req] - resp = self.dependencies_stub.GetDependencies(DependencyRequest(files=files, depth=depth)) - except Exception as e: - self.print_stderr(f'ERROR: Problem encountered sending gRPC message: {e}') + + # Determine whether to use gRPC or REST + use_grpc_flag = use_grpc if use_grpc is not None else self.use_grpc + + if use_grpc_flag: + return self._call_rpc(rpc_method, request_input, request_type, debug_msg) else: - if resp: - if not self._check_status_response(resp.status): - return None - return resp.dependencies - return None + # For REST, we only need the dict input + if not isinstance(request_input, dict): + request_input = MessageToDict(request_input, preserving_proto_field_name=True) + return self._call_rest(endpoint_key, request_input, debug_msg) + + def _call_rpc(self, rpc_method, request_input, request_type, debug_msg: Optional[str] = None) -> Optional[Dict]: + """ + Call a gRPC method and return the response as a dictionary + + Args: + rpc_method (): The gRPC stub method + request_input (): Either a dict or a gRPC request object. + request_type (): The type of the gRPC request object. + debug_msg (str, optional): Debug message template that can include {rqId} placeholder. - def _check_status_response(self, status_response: StatusResponse) -> bool: + Returns: + dict: The parsed gRPC response as a dictionary, or None if something went wrong + """ + request_id = str(uuid.uuid4()) + if isinstance(request_input, dict): + request_obj = ParseDict(request_input, request_type()) + else: + request_obj = request_input + metadata = self.metadata[:] + [('x-request-id', request_id)] + if debug_msg: + self.print_debug(debug_msg.format(rqId=request_id)) + try: + resp = rpc_method(request_obj, metadata=metadata, timeout=self.timeout) + except grpc.RpcError as e: + raise ScanossGrpcError( + f'{e.__class__.__name__} while sending gRPC message (rqId: {request_id}): {e.details()}' + ) + if resp and not self._check_status_response_grpc(resp.status, request_id): + return None + + resp_dict = MessageToDict(resp, preserving_proto_field_name=True) + return resp_dict + + def _check_status_response_grpc(self, status_response: StatusResponse, request_id: str = None) -> bool: """ Check the response object to see if the command was successful or not :param status_response: Status Response :return: True if successful, False otherwise """ + if not status_response: - self.print_stderr('Warning: No status response supplied. Assuming it was ok.') + self.print_stderr(f'Warning: No status response supplied (rqId: {request_id}). Assuming it was ok.') return True - self.print_debug(f'Checking response status: {status_response}') + self.print_debug(f'Checking response status (rqId: {request_id}): {status_response}') status_code: StatusCode = status_response.status - # self.print_stderr(f'Status Code: {status_code}, Message: {status_response.message}') - if status_code > 1: - self.print_stderr(f'Not such a success: {status_response.message}') - return False + if status_code > ScanossGrpcStatusCode.SUCCESS: + ret_val = False # default to failed + msg = 'Unsuccessful' + if status_code == ScanossGrpcStatusCode.SUCCEEDED_WITH_WARNINGS: + msg = 'Succeeded with warnings' + ret_val = True # No need to fail as it succeeded with warnings + elif status_code == ScanossGrpcStatusCode.WARNING: + msg = 'Failed with warnings' + self.print_stderr(f'{msg} (rqId: {request_id} - status: {status_code}): {status_response.message}') + return ret_val return True + + def check_status_response_rest(self, status_dict: dict, request_id: Optional[str] = None) -> bool: + """ + Check the REST response dictionary to see if the command was successful or not + + Args: + status_dict (dict): Status dictionary from REST response containing 'status' and 'message' keys + request_id (str, optional): Request ID for logging + Returns: + bool: True if successful, False otherwise + """ + if not status_dict: + self.print_stderr(f'Warning: No status response supplied (rqId: {request_id}). Assuming it was ok.') + return True + + if request_id: + self.print_debug(f'Checking response status (rqId: {request_id}): {status_dict}') + + # Get status from dictionary - it can be either a string or nested dict + status = status_dict.get('status') + message = status_dict.get('message', '') + ret_val = True + + # Handle case where status might be a string directly + if isinstance(status, str): + status_str = status.upper() + if status_str == ScanossRESTStatusCode.SUCCESS.value: + ret_val = True + elif status_str == ScanossRESTStatusCode.SUCCEEDED_WITH_WARNINGS.value: + self.print_stderr(f'Succeeded with warnings (rqId: {request_id}): {message}') + ret_val = True + elif status_str == ScanossRESTStatusCode.WARNING.value: + self.print_stderr(f'Failed with warnings (rqId: {request_id}): {message}') + ret_val = False + elif status_str == ScanossRESTStatusCode.FAILED.value: + self.print_stderr(f'Unsuccessful (rqId: {request_id}): {message}') + ret_val = False + else: + self.print_debug(f'Unknown status "{status_str}" (rqId: {request_id}). Assuming success.') + ret_val = True + + # Otherwise asume success + self.print_debug(f'Unexpected status type {type(status)} (rqId: {request_id}). Assuming success.') + return ret_val + + def _get_proxy_config(self): + """ + Set the grpc_proxy/http_proxy/https_proxy environment variables if PAC file has been specified + or if an explicit proxy has been supplied + :param self: + """ + if self.grpc_proxy: + self.print_debug('Setting GRPC (grpc_proxy) proxy...') + os.environ['grpc_proxy'] = self.grpc_proxy + elif self.proxy: + self.print_debug('Setting GRPC (http_proxy/https_proxy) proxies...') + os.environ['http_proxy'] = self.proxy + os.environ['https_proxy'] = self.proxy + elif self.pac: + self.print_debug(f'Attempting to get GRPC proxy details from PAC for {self.orig_url}...') + resolver = ProxyResolver(self.pac) + proxies = resolver.get_proxy_for_requests(self.orig_url) + if proxies: + self.print_trace(f'Setting proxies: {proxies}') + os.environ['http_proxy'] = proxies.get('http') or '' + os.environ['https_proxy'] = proxies.get('https') or '' + + def get_provenance_json(self, purls: dict, use_grpc: Optional[bool] = None) -> Optional[Dict]: + """ + Client function to call the rpc for GetComponentContributors + + Args: + purls (dict): ComponentsRequest + use_grpc (bool): Whether to use gRPC or REST (None = use instance default) + + Returns: + dict: JSON response or None + """ + return self._call_api( + 'geoprovenance.GetCountryContributorsByComponents', + self.provenance_stub.GetCountryContributorsByComponents, + purls, + ComponentsRequest, + 'Sending data for provenance decoration (rqId: {rqId})...', + use_grpc=use_grpc, + ) + + def get_provenance_origin(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]: + """ + Client function to call the rpc for GetOriginByComponents + + Args: + request (Dict): GetOriginByComponents Request + + Returns: + Optional[Dict]: OriginResponse, or None if the request was not successfull + """ + return self._call_api( + 'geoprovenance.GetOriginByComponents', + self.provenance_stub.GetOriginByComponents, + request, + ComponentsRequest, + 'Sending data for provenance origin decoration (rqId: {rqId})...', + use_grpc=use_grpc, + ) + + def get_crypto_algorithms_for_purl(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]: + """ + Client function to call the rpc for GetComponentsAlgorithms for a list of purls + + Args: + request (Dict): ComponentsRequest + use_grpc (Optional[bool]): Whether to use gRPC or REST (None = use instance default) + + Returns: + Optional[Dict]: AlgorithmResponse, or None if the request was not successfull + """ + return self._call_api( + 'cryptography.GetComponentsAlgorithms', + self.crypto_stub.GetComponentsAlgorithms, + request, + ComponentsRequest, + 'Sending data for cryptographic algorithms decoration (rqId: {rqId})...', + use_grpc=use_grpc, + ) + + def get_crypto_algorithms_in_range_for_purl(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]: + """ + Client function to call the rpc for GetComponentsAlgorithmsInRange for a list of purls + + Args: + request (Dict): ComponentsRequest + use_grpc (Optional[bool]): Whether to use gRPC or REST (None = use instance default) + + Returns: + Optional[Dict]: AlgorithmsInRangeResponse, or None if the request was not successfull + """ + return self._call_api( + 'cryptography.GetComponentsAlgorithmsInRange', + self.crypto_stub.GetComponentsAlgorithmsInRange, + request, + ComponentsRequest, + 'Sending data for cryptographic algorithms in range decoration (rqId: {rqId})...', + use_grpc=use_grpc, + ) + + def get_encryption_hints_for_purl(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]: + """ + Client function to call the rpc for GetComponentsEncryptionHints for a list of purls + + Args: + request (Dict): ComponentsRequest + use_grpc (Optional[bool]): Whether to use gRPC or REST (None = use instance default) + + Returns: + Optional[Dict]: HintsResponse, or None if the request was not successfull + """ + return self._call_api( + 'cryptography.GetComponentsEncryptionHints', + self.crypto_stub.GetComponentsEncryptionHints, + request, + ComponentsRequest, + 'Sending data for encryption hints decoration (rqId: {rqId})...', + use_grpc=use_grpc, + ) + + def get_encryption_hints_in_range_for_purl(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]: + """ + Client function to call the rpc for GetComponentsHintsInRange for a list of purls + + Args: + request (Dict): ComponentsRequest + use_grpc (Optional[bool]): Whether to use gRPC or REST (None = use instance default) + + Returns: + Optional[Dict]: HintsInRangeResponse, or None if the request was not successfull + """ + return self._call_api( + 'cryptography.GetComponentsHintsInRange', + self.crypto_stub.GetComponentsHintsInRange, + request, + ComponentsRequest, + 'Sending data for encryption hints in range decoration (rqId: {rqId})...', + use_grpc=use_grpc, + ) + + def get_versions_in_range_for_purl(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]: + """ + Client function to call the rpc for GetComponentsVersionsInRange for a list of purls + + Args: + request (Dict): ComponentsRequest + use_grpc (Optional[bool]): Whether to use gRPC or REST (None = use instance default) + + Returns: + Optional[Dict]: VersionsInRangeResponse, or None if the request was not successfull + """ + return self._call_api( + 'cryptography.GetComponentsVersionsInRange', + self.crypto_stub.GetComponentsVersionsInRange, + request, + ComponentsRequest, + 'Sending data for cryptographic versions in range decoration (rqId: {rqId})...', + use_grpc=use_grpc, + ) + + def get_licenses(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]: + """ + Client function to call the rpc for Licenses GetComponentsLicenses + It will either use REST (default) or gRPC depending on the use_grpc flag + + Args: + request (Dict): ComponentsRequest + Returns: + Optional[Dict]: ComponentsLicenseResponse, or None if the request was not successfull + """ + return self._call_api( + 'licenses.GetComponentsLicenses', + self.license_stub.GetComponentsLicenses, + request, + ComponentsRequest, + 'Sending data for license decoration (rqId: {rqId})...', + use_grpc=use_grpc, + ) + + def get_component_status(self, request: Dict, use_grpc: Optional[bool] = None) -> Optional[Dict]: + """ + Client function to call the API for Components GetComponentsStatus + Only REST API is supported for this endpoint (gRPC not yet available) + + Args: + request (Dict): ComponentsRequest + Returns: + Optional[Dict]: ComponentsStatusResponse, or None if the request was not successful + """ + # Force REST API since gRPC is not available for this endpoint + if use_grpc: + self.print_stderr('WARNING: gRPC is not supported for component status. Using REST API instead.') + + return self._call_api( + 'components.GetComponentsStatus', + None, # No gRPC method available + request, + ComponentsRequest, + 'Sending data for component status retrieval (rqId: {rqId})...', + use_grpc=False, # Force REST + ) + + def load_generic_headers(self, url: Optional[str] = None): + """ + Adds custom headers from req_headers to metadata. + + If x-api-key is present and no URL is configured (directly or via + environment), sets URL to the premium endpoint (DEFAULT_URL2). + """ + if self.req_headers: # Load generic headers + for key, value in self.req_headers.items(): + if key == 'x-api-key': # Set premium URL if x-api-key header is set + if not url and not os.environ.get('SCANOSS_GRPC_URL'): + self.url = DEFAULT_URL2 # API key specific and no alternative URL, so use the default premium + self.api_key = value + self.metadata.append((key, value)) + self.headers[key] = value + + # + # End of gRPC Client Functions + # + # Start of REST Client Functions + # + + def _rest_get(self, uri: str, request_id: str, params: Optional[dict] = None) -> Optional[dict]: + """ + Send a GET request to the specified URI with optional query parameters. + + Args: + uri (str): URI to send GET request to + request_id (str): request id + params (dict, optional): Optional query parameters as dictionary + + Returns: + dict: JSON response or None + """ + if not uri: + self.print_stderr('Error: Missing URI. Cannot perform GET request.') + return None + self.print_trace(f'Sending REST GET request to {uri}...') + headers = self.headers.copy() + headers['x-request-id'] = request_id + retry = 0 + while retry <= self.retry_limit: + retry += 1 + try: + response = self.session.get(uri, headers=headers, params=params, timeout=self.timeout) + response.raise_for_status() # Raises an HTTPError for bad responses + return response.json() + except (requests.exceptions.SSLError, requests.exceptions.ProxyError) as e: + self.print_stderr(f'ERROR: Exception ({e.__class__.__name__}) sending GET request - {e}.') + raise Exception(f'ERROR: The SCANOSS API GET request failed for {uri}') from e + except requests.exceptions.HTTPError as e: + self.print_stderr(f'ERROR: HTTP error sending GET request ({request_id}): {e}') + raise Exception( + f'ERROR: The SCANOSS API GET request failed with status {e.response.status_code} for {uri}' + ) from e + except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e: + if retry > self.retry_limit: # Timed out retry_limit or more times, fail + self.print_stderr(f'ERROR: {e.__class__.__name__} sending GET request ({request_id}): {e}') + raise Exception( + f'ERROR: The SCANOSS API GET request timed out ({e.__class__.__name__}) for {uri}' + ) from e + else: + self.print_stderr(f'Warning: {e.__class__.__name__} communicating with {self.url}. Retrying...') + time.sleep(5) + except requests.exceptions.RequestException as e: + self.print_stderr(f'Error: Problem sending GET request to {uri}: {e}') + raise Exception(f'ERROR: The SCANOSS API GET request failed for {uri}') from e + except Exception as e: + self.print_stderr( + f'ERROR: Exception ({e.__class__.__name__}) sending GET request ({request_id}) to {uri}: {e}' + ) + raise Exception(f'ERROR: The SCANOSS API GET request failed for {uri}') from e + return None + + def _rest_post(self, uri: str, request_id: str, data: dict) -> Optional[dict]: + """ + Post the specified data to the given URI. + + Args: + uri (str): URI to post to + request_id (str): request id + data (dict): json data to post + + Returns: + dict: JSON response or None + """ + if not uri: + self.print_stderr('Error: Missing URI. Cannot search for project.') + return None + self.print_trace(f'Sending REST POST data to {uri}...') + headers = self.headers.copy() + headers['x-request-id'] = request_id + retry = 0 + while retry <= self.retry_limit: + retry += 1 + try: + response = self.session.post(uri, headers=headers, json=data, timeout=self.timeout) + response.raise_for_status() # Raises an HTTPError for bad responses + return response.json() + except (requests.exceptions.SSLError, requests.exceptions.ProxyError) as e: + self.print_stderr(f'ERROR: Exception ({e.__class__.__name__}) POSTing data - {e}.') + raise Exception(f'ERROR: The SCANOSS Decoration API request failed for {uri}') from e + except requests.exceptions.HTTPError as e: + self.print_stderr(f'ERROR: HTTP error POSTing data ({request_id}): {e}') + raise Exception( + f'ERROR: The SCANOSS Decoration API request failed with status {e.response.status_code} for {uri}' + ) from e + except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e: + if retry > self.retry_limit: # Timed out retry_limit or more times, fail + self.print_stderr(f'ERROR: {e.__class__.__name__} POSTing decoration data ({request_id}): {e}') + raise Exception( + f'ERROR: The SCANOSS Decoration API request timed out ({e.__class__.__name__}) for {uri}' + ) from e + else: + self.print_stderr(f'Warning: {e.__class__.__name__} communicating with {self.url}. Retrying...') + time.sleep(5) + except requests.exceptions.RequestException as e: + self.print_stderr(f'Error: Problem posting data to {uri}: {e}') + raise Exception(f'ERROR: The SCANOSS Decoration API request failed for {uri}') from e + except Exception as e: + self.print_stderr( + f'ERROR: Exception ({e.__class__.__name__}) POSTing data ({request_id}) to {uri}: {e}' + ) + raise Exception(f'ERROR: The SCANOSS Decoration API request failed for {uri}') from e + return None + + def _call_rest(self, endpoint_key: str, request_input: dict, debug_msg: Optional[str] = None) -> Optional[Dict]: + """ + Call a REST endpoint and return the response as a dictionary + + Args: + endpoint_key (str): The key to lookup the REST endpoint in REST_ENDPOINTS + request_input (dict): The request data to send + debug_msg (str, optional): Debug message template that can include {rqId} placeholder. + + Returns: + dict: The parsed REST response as a dictionary, or None if something went wrong + """ + if endpoint_key not in REST_ENDPOINTS: + raise ScanossGrpcError(f'Unknown REST endpoint key: {endpoint_key}') + + endpoint_config = REST_ENDPOINTS[endpoint_key] + endpoint_path = endpoint_config['path'] + method = endpoint_config['method'] + endpoint_url = f'{self.orig_url}{DEFAULT_URI_PREFIX}{endpoint_path}' + request_id = str(uuid.uuid4()) + + if debug_msg: + self.print_debug(debug_msg.format(rqId=request_id)) + + if method == 'GET': + response = self._rest_get(endpoint_url, request_id, params=request_input) + else: # POST + response = self._rest_post(endpoint_url, request_id, request_input) + + if response and 'status' in response and not self.check_status_response_rest(response['status'], request_id): + return None + + return response + + # # End of ScanossGrpc Class # + + +@dataclass +class GrpcConfig: + url: str = DEFAULT_URL + api_key: Optional[str] = SCANOSS_API_KEY + debug: Optional[bool] = False + trace: Optional[bool] = False + quiet: Optional[bool] = False + ver_details: Optional[str] = None + ca_cert: Optional[str] = None + timeout: Optional[int] = DEFAULT_TIMEOUT + proxy: Optional[str] = None + grpc_proxy: Optional[str] = None + pac: Optional[PACFile] = None + req_headers: Optional[dict] = None + + +def create_grpc_config_from_args(args) -> GrpcConfig: + return GrpcConfig( + url=getattr(args, 'api2url', DEFAULT_URL), + api_key=getattr(args, 'key', SCANOSS_API_KEY), + debug=getattr(args, 'debug', False), + trace=getattr(args, 'trace', False), + quiet=getattr(args, 'quiet', False), + ver_details=getattr(args, 'ver_details', None), + ca_cert=getattr(args, 'ca_cert', None), + timeout=getattr(args, 'timeout', DEFAULT_TIMEOUT), + proxy=getattr(args, 'proxy', None), + grpc_proxy=getattr(args, 'grpc_proxy', None), + ) + + +# +# End of GrpcConfig class +# diff --git a/src/scanoss/scanpostprocessor.py b/src/scanoss/scanpostprocessor.py new file mode 100644 index 00000000..182b17eb --- /dev/null +++ b/src/scanoss/scanpostprocessor.py @@ -0,0 +1,292 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2024, SCANOSS + + 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. +""" + +from typing import List, Optional + +from packageurl import PackageURL +from packageurl.contrib import purl2url + +from .scanoss_settings import BomEntry, ReplaceRule, ScanossSettings, find_best_match +from .scanossbase import ScanossBase + +COMPONENT_LEVEL_FIELDS = ( + 'component', 'vendor', 'url', 'url_hash', 'version', 'latest', + 'release_date', 'licenses', 'url_stats', 'cryptography', + 'vulnerabilities', 'provenance', 'dependencies', 'health', + 'quality', +) + + +def _get_match_type_message(result_path: str, bom_entry: BomEntry, action: str) -> str: + """ + Compose message based on match type + + Args: + result_path (str): Path of the scan result + bom_entry (BomEntry): BOM entry to compare with + action (str): Post processing action being performed + + Returns: + str: The message to be printed + """ + entry_path = bom_entry.path or '' + if entry_path and bom_entry.purl: + # Result keys are always file paths, so exact match means file-level rule; + # otherwise the match came via folder prefix. + match_kind = 'file' if entry_path == result_path else 'folder' + message = f"{action} '{result_path}'. Full match found ({match_kind} + purl)." + elif bom_entry.purl: + message = f"{action} '{result_path}'. Found PURL match." + else: + message = f"{action} '{result_path}'. Found path match." + return message + + +class ScanPostProcessor(ScanossBase): + """ + Handles post-processing of the scan results + """ + + def __init__( + self, + scanoss_settings: ScanossSettings, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + results: dict = None, + ): + """ + Args: + scanoss_settings (ScanossSettings): Scanoss settings object + debug (bool, optional): Debug mode. Defaults to False. + trace (bool, optional): Traces. Defaults to False. + quiet (bool, optional): Quiet mode. Defaults to False. + results (dict | str, optional): Results to be processed. Defaults to None. + """ + super().__init__(debug, trace, quiet) + self.scanoss_settings = scanoss_settings + self.results: dict = results + self.component_info_map: dict = {} + + def load_results(self, raw_results: dict): + """Load the raw results + + Args: + raw_results (dict): Raw scan results + """ + self.results = raw_results + self._load_component_info() + return self + + def _load_component_info(self): + """Create a map of component information from scan results for faster lookup""" + if not self.results: + return + for _, result in self.results.items(): + entry = result[0] if isinstance(result, list) else result + purls = entry.get('purl', []) + for purl in purls: + self.component_info_map[purl] = entry + + def post_process(self): + """ + Post-process the scan results + + Returns: + dict: Processed results + """ + if not self.scanoss_settings: + return self.results + if self.scanoss_settings.is_legacy(): + self.print_stderr( + 'Legacy settings file detected. Post-processing is not supported for legacy settings file.' + ) + return self.results + self._remove_dismissed_files() + self._apply_replace_rules() + return self.results + + def _remove_dismissed_files(self): + """ + Remove entries from the results based on files and/or purls specified in the SCANOSS settings file + """ + to_remove_entries = self.scanoss_settings.get_bom_remove() + if not to_remove_entries: + return + self.results = { + result_path: result + for result_path, result in self.results.items() + if not self._should_remove_result(result_path, result, to_remove_entries) + } + + def _apply_replace_rules(self): + """ + Apply BOM replace rules from the SCANOSS settings file to the scan results + """ + to_replace_entries = self.scanoss_settings.get_bom_replace() + if not to_replace_entries: + return + + for result_path, result in self.results.items(): + entry = result[0] if isinstance(result, list) else result + replace_rule = self._find_replace_rule(result_path, entry, to_replace_entries) + if replace_rule: + self.results[result_path] = [self._apply_replace_rule(entry, replace_rule)] + + def _apply_replace_rule(self, result: dict, replace_rule: ReplaceRule) -> dict: + """ + Update the result with the new purl and component information if available, + otherwise removes the old component information + + Args: + result (dict): The result to update + replace_rule (ReplaceRule): The replace rule to apply + + Returns: + dict: Updated result + """ + if self.component_info_map.get(replace_rule.replace_with): + # Only copy component-level fields from the map entry, leaving + # per-file fields (file, file_hash, lines, matched, etc.) untouched. + source = self.component_info_map[replace_rule.replace_with] + for key in COMPONENT_LEVEL_FIELDS: + if key in source: + result[key] = source[key] + else: + try: + new_component = PackageURL.from_string(replace_rule.replace_with).to_dict() + new_component_url = purl2url.get_repo_url(replace_rule.replace_with) + except (ValueError, RuntimeError): + self.print_stderr( + f"ERROR: Issue while replacing: Invalid PURL '{replace_rule.replace_with}'" + ' in settings file. Skipping.' + ) + return result + + # Reset component-level fields to defaults + result['licenses'] = [] + result['cryptography'] = [] + result['dependencies'] = [] + result['quality'] = [] + result['vulnerabilities'] = [] + result['health'] = {} + result['provenance'] = '' + result['latest'] = '' + result['release_date'] = '' + result['version'] = '' + result['url_hash'] = '' + result['url_stats'] = {} + + # Set what we know from the PURL + result['component'] = new_component.get('name') + result['url'] = new_component_url + result['vendor'] = new_component.get('namespace') + + + if replace_rule.license: + result['licenses'] = [{'name': replace_rule.license}] + elif not self.component_info_map.get(replace_rule.replace_with): + result['licenses'] = [] + + result['purl'] = [replace_rule.replace_with] + result['status'] = 'identified' + + return result + + def _find_replace_rule( + self, result_path: str, result: dict, to_replace_entries: List[ReplaceRule] + ) -> Optional[ReplaceRule]: + """ + Check if a result should be replaced based on the SCANOSS settings. + Uses priority-based matching: most specific rule wins. + + Args: + result_path (str): Path of the result data + result (dict): Result to check + to_replace_entries (List[ReplaceRule]): Replace rules from the settings file + + Returns: + Optional[ReplaceRule]: The matching replace rule, or None if no match + """ + result_purls = result.get('purl', []) + match = find_best_match(result_path, result_purls, to_replace_entries) + if match and isinstance(match, ReplaceRule) and match.replace_with: + if self.debug: + self._print_message(result_path, result_purls, match, 'Replacing') + return match + return None + + def _should_remove_result(self, result_path: str, result: dict, to_remove_entries: List[BomEntry]) -> bool: + """ + Check if a result should be removed based on the SCANOSS settings. + Uses priority-based matching: most specific rule wins. + + Args: + result_path (str): Path of the result data + result (dict): Result to check + to_remove_entries (List[BomEntry]): BOM entries to remove from the result + + Returns: + True if the result should be removed + """ + result = result[0] if isinstance(result, list) else result + result_purls = result.get('purl', []) + match = find_best_match(result_path, result_purls, to_remove_entries) + if match: + if self.debug: + self._print_message(result_path, result_purls, match, 'Removing') + return True + return False + + def _print_message(self, result_path: str, result_purls: List[str], bom_entry: BomEntry, action: str) -> None: + """ + Print a message about replacing or removing a result + + Args: + result_path (str): Path of the scan result + result_purls (List[str]): Purls from the scan result + bom_entry (BomEntry): Matched BOM entry + action (str): Action being performed + """ + if not self.debug: + return + if action == 'Replacing' and isinstance(bom_entry, ReplaceRule): + message = ( + f'{_get_match_type_message(result_path, bom_entry, action)}\n' + f'Details:\n' + f' - PURLs: {", ".join(result_purls)}\n' + f' - Replace with: {bom_entry.replace_with}\n' + f" - Path: '{result_path}'" + ) + else: + message = ( + f'{_get_match_type_message(result_path, bom_entry, action)}\n' + f'Details:\n' + f' - PURLs: {", ".join(result_purls)}\n' + f" - Path: '{result_path}'" + ) + self.print_debug(message) +# +# End of ScanPostProcessor Class +# diff --git a/src/scanoss/scantype.py b/src/scanoss/scantype.py index 67effb41..9e617c66 100644 --- a/src/scanoss/scantype.py +++ b/src/scanoss/scantype.py @@ -1,25 +1,25 @@ """ - SPDX-License-Identifier: MIT - - Copyright (c) 2021, SCANOSS - - 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. +SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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. """ from enum import Enum @@ -29,6 +29,7 @@ class ScanType(Enum): """ Octal Enum class describing all the scanning options """ + SCAN_FILES = 1 SCAN_SNIPPETS = 2 SCAN_DEPENDENCIES = 4 diff --git a/src/scanoss/services/dependency_track_service.py b/src/scanoss/services/dependency_track_service.py new file mode 100644 index 00000000..debc287d --- /dev/null +++ b/src/scanoss/services/dependency_track_service.py @@ -0,0 +1,132 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +import requests + +from ..scanossbase import ScanossBase + +HTTP_OK = 200 + +class DependencyTrackService(ScanossBase): + + def __init__( + self, + api_key: str, + url: str, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + ): + super().__init__(debug=debug, trace=trace, quiet=quiet) + if not url: + raise ValueError("Error: Dependency Track URL is required") + self.url = url.strip().rstrip('/') + if not api_key: + raise ValueError("Error: Dependency Track API key is required") + self.api_key = api_key + + def get_project_by_name_version(self, name, version): + """ + Get project information by name and version from Dependency Track + + Args: + name: Project name to search for + version: Project version to search for + + Returns: + dict: Project data if found, None otherwise + """ + if not name or not version: + self.print_stderr('Error: Missing name or version.') + return None + # Use the project search endpoint + params = { + 'name': name, + 'version': version + } + self.print_debug(f'Searching for project by: {params}') + return self.get_dep_track_data(f'{self.url}/api/v1/project/lookup', params) + + def get_project_status(self, upload_token): + """ + Get Dependency Track project processing status. + + Queries the Dependency Track API to check if the project upload + processing is complete using the upload token. + + Returns: + dict: Project status information or None if request fails + """ + if not upload_token: + self.print_stderr('Error: Missing upload token. Cannot search for project status.') + return None + self.print_trace(f'URL: {self.url} Upload token: {upload_token}') + return self.get_dep_track_data(f'{self.url}/api/v1/event/token/{upload_token}') + + def get_project_violations(self,project_id:str): + """ + Get project violations from Dependency Track. + + Waits for project processing to complete, then retrieves all policy + violations for the specified project ID. + + Returns: + List of policy violations or None if the request fails + """ + if not project_id: + self.print_stderr('Error: Missing project id. Cannot search for project violations.') + return None + # Return the result as-is - None indicates API failure, empty list means no violations + return self.get_dep_track_data(f'{self.url}/api/v1/violation/project/{project_id}') + + def get_project_by_id(self, project_id:str): + """ + Get a Dependency Track project by id. + + Queries the Dependency Track API to get a project by id + + Returns: + dict + """ + if not project_id: + self.print_stderr('Error: Missing project id. Cannot search for project.') + return None + self.print_trace(f'URL: {self.url}, UUID: {project_id}') + return self.get_dep_track_data(f'{self.url}/api/v1/project/{project_id}') + + def get_dep_track_data(self, uri, params=None): + if not uri: + self.print_stderr('Error: Missing URI. Cannot search for project.') + return None + req_headers = {'X-Api-Key': self.api_key, 'Content-Type': 'application/json'} + try: + if params: + response = requests.get(uri, headers=req_headers, params=params) + else: + response = requests.get(uri, headers=req_headers) + response.raise_for_status() # Raises an HTTPError for bad responses + return response.json() + except requests.exceptions.RequestException as e: + self.print_stderr(f"Error: Problem getting project data: {e}") + return None diff --git a/src/scanoss/spdxlite.py b/src/scanoss/spdxlite.py index b24ef803..89bd70ef 100644 --- a/src/scanoss/spdxlite.py +++ b/src/scanoss/spdxlite.py @@ -1,34 +1,37 @@ """ - SPDX-License-Identifier: MIT - - Copyright (c) 2021, SCANOSS - - 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. +SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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. """ -import json -import os.path -import sys -import hashlib + import datetime import getpass +import hashlib +import json +import os.path import re -import pkg_resources +import sys + +import importlib_resources +from packageurl import PackageURL from . import __version__ @@ -68,76 +71,225 @@ def parse(self, data: json): :param data: json - JSON object :return: summary dictionary """ - if not data: + if data is None: self.print_stderr('ERROR: No JSON data provided to parse.') return None - self.print_debug(f'Processing raw results into summary format...') + if len(data) == 0: + self.print_debug('Warning: Empty scan results provided. Returning empty summary.') + return {} + + self.print_debug('Processing raw results into summary format...') + return self._process_files(data) + + def _process_files(self, data: json) -> dict: + """ + Process raw results and build a component summary. + + Args: + data: JSON data containing raw results + + Returns: + dict: The built summary dictionary + """ summary = {} - for f in data: - file_details = data.get(f) - # print(f'File: {f}: {file_details}\n') - for d in file_details: - id_details = d.get("id") - if not id_details or id_details == 'none': # Ignore files with no ids - continue - purl = None - if id_details == 'dependency': # Process dependency data - dependencies = d.get("dependencies") - if not dependencies: - self.print_stderr(f'Warning: No Dependencies found for {f}: {file_details}') - continue - for deps in dependencies: - # print(f'File: {f} Deps: {deps}') - purl = deps.get("purl") - if not purl: - self.print_stderr(f'Warning: No PURL found for {f}: {deps}') - continue - if summary.get(purl): - self.print_debug(f'Component {purl} already stored: {summary.get(purl)}') - continue - fd = {} - for field in ['component', 'version', 'url']: - fd[field] = deps.get(field, '') - licenses = deps.get('licenses') - fdl = [] - dc = [] - for lic in licenses: - name = lic.get("name") - if name not in dc: # Only save the license name once - fdl.append({'id': name}) - dc.append(name) - fd['licenses'] = fdl - summary[purl] = fd - else: # Normal file id type - purls = d.get('purl') - if not purls: - self.print_stderr(f'Purl block missing for {f}: {file_details}') - continue - for p in purls: - self.print_debug(f'Purl: {p}') - purl = p - break - if not purl: - self.print_stderr(f'Warning: No PURL found for {f}: {file_details}') - continue - if summary.get(purl): - self.print_debug(f'Component {purl} already stored: {summary.get(purl)}') - continue - fd = {} - for field in ['id', 'vendor', 'component', 'version', 'latest', 'url']: - fd[field] = d.get(field) - licenses = d.get('licenses') - fdl = [] - dc = [] - for lic in licenses: - name = lic.get("name") - if name not in dc: # Only save the license name once - fdl.append({'id': name}) - dc.append(name) - fd['licenses'] = fdl - summary[purl] = fd + for file_path in data: + file_details = data.get(file_path) + # summary is passed by reference and modified inside the function + self._process_entries(file_path, file_details, summary) return summary + def _process_entries(self, file_path: str, file_details: list, summary: dict): + """ + Process entries for a single file. + + Args: + file_path: Path to the file being processed + file_details: Results of the file + summary: Reference to summary dictionary that will be modified in place + """ + for entry in file_details: + id_details = entry.get('id') + if not id_details or id_details == 'none': + continue + + if id_details == 'dependency': + self._process_dependency_entry(file_path, entry, summary) + else: + self._process_file_entry(file_path, entry, summary) + + def _process_dependency_entry(self, file_path: str, entry: dict, summary: dict): + """ + Process a dependency type entry. + + Args: + file_path: Path to the file being processed + entry: The dependency entry to process + summary: Reference to summary dictionary that will be modified in place + """ + dependencies = entry.get('dependencies') + if not dependencies: + self.print_stderr(f'Warning: No Dependencies found for {file_path}') + return + + for dep in dependencies: + purl = dep.get('purl') + if not self._is_valid_purl(file_path, dep, purl, summary): + continue + # Modifying the summary dictionary directly as it's passed by reference + summary[purl] = self._create_dependency_summary(dep) + + def _process_file_entry(self, file_path: str, entry: dict, summary: dict): + """ + Process file entry. + + Args: + file_path: Path to the file being processed + entry: Process file match entry + summary: Reference to summary dictionary that will be modified in place + """ + purls = entry.get('purl') + if not purls: + self.print_stderr(f'Purl block missing for {file_path}') + return + + purl = purls[0] if purls else None + if not self._is_valid_purl(file_path, entry, purl, summary): + return + + summary[purl] = self._create_file_summary(entry) + + def _is_valid_purl(self, file_path: str, entry: dict, purl: str, summary: dict) -> bool: + """ + Check if purl is valid and not already processed. + + Args: + file_path: Path to the file being processed + entry: The entry containing the PURL + purl: The PURL to validate + summary: Reference to summary dictionary to check for existing entries + + Returns: + bool: True if purl is valid and not already processed + """ + if not purl: + self.print_stderr(f'Warning: No PURL found for {file_path}: {entry}') + return False + + if summary.get(purl): + self.print_debug(f'Component {purl} already stored: {summary.get(purl)}') + return False + + return True + + def _create_dependency_summary(self, dep: dict) -> dict: + """ + Create summary for dependency entry. + + This method extracts relevant fields from a dependency entry and creates a + standardized summary dictionary. It handles fields like component, version, + and URL, with special processing for licenses. + + Args: + dep (dict): The dependency entry containing component information + + Returns: + dict: A new summary dictionary containing the extracted and processed fields + """ + summary = {} + for field in ['component', 'version', 'url']: + summary[field] = dep.get(field, '') + summary['licenses'] = self._process_licenses(dep.get('licenses')) + return summary + + def _create_file_summary(self, entry: dict) -> dict: + """ + Create summary for file entry. + + This method extracts set of fields from file entry and creates a standardized summary dictionary. + + Args: + entry (dict): The file entry containing the metadata to summarize + + Returns: + dict: A new summary dictionary containing all extracted and processed fields + """ + summary = {} + fields = ['id', 'vendor', 'component', 'version', 'latest', + 'url', 'url_hash', 'download_url'] + for field in fields: + summary[field] = entry.get(field) + summary['licenses'] = self._process_licenses(entry.get('licenses')) + summary['cpes'] = self._extract_cpes(entry.get('vulnerabilities')) + return summary + + def _extract_cpes(self, vulnerabilities: list) -> list: + """ + Extract CPE identifiers from a file entry's vulnerabilities array. + + Raw scan results deliver CPEs embedded as vulnerability IDs prefixed with "CPE:" + (case-insensitive). Everything else in the array is a real vulnerability record + (CVE/GHSA) and must be ignored here. + + Args: + vulnerabilities (list): The 'vulnerabilities' list from a file match entry. + May be None or empty. + + Returns: + list: Deduplicated list of CPE strings in source order (e.g. + ['cpe:2.3:a:postgresql:postgresql:17.0:*:*:*:*:*:*:*']). + Returns an empty list when there are no CPE entries. + """ + if not vulnerabilities: + return [] + cpes = [] + seen = set() + for vuln in vulnerabilities: + vuln_id = vuln.get('ID') or vuln.get('id') or '' + if not vuln_id.upper().startswith('CPE:'): + continue + normalized = vuln_id.upper() + if normalized in seen: + continue + seen.add(normalized) + cpes.append(vuln_id) + return cpes + + def _process_licenses(self, licenses: list) -> list: + """ + Process license information and remove duplicates. + + This method filters license information to include only licenses from trusted sources + ('component_declared', 'license_file', 'file_header'). Licenses with an unspecified + source (None or '') are allowed. Non-empty, non-allowed sources are excluded. It also + removes any duplicate license names. + The result is a simplified list of license dictionaries containing only the 'id' field. + + Args: + licenses (list): A list of license dictionaries, each containing at least 'name' + and 'source' fields. Can be None or empty. + + Returns: + list: A filtered and deduplicated list of license dictionaries, where each + dictionary contains only an 'id' field matching the original license name. + Returns an empty list if input is None or empty. + """ + if not licenses: + return [] + + processed_licenses = [] + seen_names = set() + + for license_info in licenses: + name = license_info.get('name') + source = license_info.get('source') + if source not in (None, '') and source not in ("component_declared", "license_file", "file_header"): + continue + if name and name not in seen_names: + processed_licenses.append({'id': name}) + seen_names.add(name) + + return processed_licenses + def produce_from_file(self, json_file: str, output_file: str = None) -> bool: """ Parse plain/raw input JSON file and produce SPDX Lite output @@ -157,81 +309,390 @@ def produce_from_file(self, json_file: str, output_file: str = None) -> bool: def produce_from_json(self, data: json, output_file: str = None) -> bool: """ - Produce the SPDX Lite output from the input JSON object + Produce the SPDX Lite output from the input data :param data: JSON object :param output_file: Output file (optional) :return: True if successful, False otherwise """ raw_data = self.parse(data) - if not raw_data: + if raw_data is None: self.print_stderr('ERROR: No SPDX data returned for the JSON string provided.') return False + if len(raw_data) == 0: + self.print_debug('Warning: Empty scan results - generating minimal SPDX Lite document with no packages.') + self.load_license_data() - # Using this SPDX version as the spec - # https://github.com/spdx/spdx-spec/blob/development/v2.2.2/examples/SPDXJSONExample-v2.2.spdx.json - # Validate using: - # pip3 install jsonschema - # jsonschema -i spdxlite.json <(curl https://raw.githubusercontent.com/spdx/spdx-spec/v2.2/schemas/spdx-schema.json) - # Validation can also be done online here: https://tools.spdx.org/app/validate/ + spdx_document = self._create_base_document(raw_data) + self._process_packages(raw_data, spdx_document) + return self._write_output(spdx_document, output_file) + + def _create_base_document(self, raw_data: dict) -> dict: + """ + Create the base SPDX document structure. + + This method initializes a new SPDX document with standard fields required by + the SPDX 2.2 specification. It generates a unique document namespace using + a hash of the raw data and current timestamp. + + Args: + raw_data (dict): The raw component data used to create a unique identifier + for the document namespace + + Returns: + dict: A dictionary containing the base SPDX document structure with the + following fields: + - spdxVersion: The SPDX specification version + - dataLicense: The license for the SPDX document itself + - SPDXID: The document's unique identifier + - name: The name of the SBOM + - creationInfo: Information about when and how the document was created + - documentNamespace: A unique URI for this document + - documentDescribes: List of packages described (initially empty) + - hasExtractedLicensingInfos: List of licenses (initially empty) + - packages: List of package information (initially empty) + """ now = datetime.datetime.utcnow() md5hex = hashlib.md5(f'{raw_data}-{now}'.encode('utf-8')).hexdigest() - data = { + + return { 'spdxVersion': 'SPDX-2.2', 'dataLicense': 'CC0-1.0', - 'SPDXID': f'SPDXRef-{md5hex}', + 'SPDXID': 'SPDXRef-DOCUMENT', 'name': 'SCANOSS-SBOM', - 'creationInfo': { - 'created': now.strftime('%Y-%m-%dT%H:%M:%S') + now.strftime('.%f')[:4] + 'Z', - 'creators': [f'Tool: SCANOSS-PY: {__version__}', f'User: {getpass.getuser()}'] - }, + 'creationInfo': self._create_creation_info(now), 'documentNamespace': f'https://spdx.org/spdxdocs/scanoss-py-{__version__}-{md5hex}', - 'packages': [] + 'documentDescribes': [], + 'hasExtractedLicensingInfos': [], + 'packages': [], } - for purl in raw_data: - comp = raw_data.get(purl) - lic_names = [] - licenses = comp.get('licenses') - lic_text = 'NOASSERTION' - if licenses: - for lic in licenses: - lc_id = lic.get('id') - if lc_id: - spdx_id = self.get_spdx_license_id(lc_id) - lic_names.append(spdx_id if spdx_id else lc_id) - if len(lic_names) > 0: - lic_text = ' AND '.join(lic_names) - if len(lic_names) > 1: - lic_text = f'({lic_text})' # wrap the names in () if there is more than one - comp_name = comp.get('component') - comp_ver = comp.get('version') - purl_ver = f'{purl}@{comp_ver}' - purl_hash = hashlib.md5(f'{purl_ver}'.encode('utf-8')).hexdigest() - data['packages'].append({ - 'name': comp_name, - 'SPDXID': f'SPDXRef-{purl_hash}', - 'versionInfo': comp_ver, - 'downloadLocation': 'NOASSERTION', # TODO Add actual download location - 'homepage': comp.get('url', ''), - 'licenseDeclared': lic_text, - 'licenseConcluded': 'NOASSERTION', - 'filesAnalyzed': False, - 'copyrightText': 'NOASSERTION', - 'externalRefs': [{ - 'referenceCategory': 'PACKAGE_MANAGER', - 'referenceLocator': purl_ver, - 'referenceType': 'purl' - }] + + def _create_creation_info(self, timestamp: datetime.datetime) -> dict: + """ + Create the creation info section of an SPDX document. + + This method generates the creation information required by the SPDX specification, + including timestamps, creator information, and document type. + + Args: + timestamp (datetime.datetime): The UTC timestamp representing when the + document was created + + Returns: + dict: A dictionary containing creation information with the following fields: + - created: ISO 8601 formatted timestamp + - creators: List of entities involved in creating the document + (tool, person, and organization) + - comment: Additional information about the SBOM type + """ + return { + 'created': timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'creators': [ + f'Tool: SCANOSS-PY: {__version__}', + f'Person: {getpass.getuser()}', + 'Organization: SCANOSS' + ], + 'comment': 'SBOM Build information - SBOM Type: Build', + } + + def _process_packages(self, raw_data: dict, spdx_document: dict): + """ + Process packages and add them to the SPDX document. + + This method iterates through the raw component data, creates package information + for each component, and adds them to the SPDX document. It also collects + license references to be processed separately. + + Args: + raw_data (dict): Dictionary of package data indexed by PURL + (Package URL identifiers) + spdx_document (dict): Reference to the SPDX document being built, + which will be modified in place + + Note: + This method modifies the spdx_document dictionary in place by: + 1. Adding package information to the 'packages' list + 2. Adding package SPDXIDs to the 'documentDescribes' list + 3. Indirectly populating 'hasExtractedLicensingInfos' via _process_license_refs() + """ + lic_refs = set() + + for purl, comp in raw_data.items(): + package_info = self._create_package_info(purl, comp, lic_refs) + spdx_document['packages'].append(package_info) + spdx_document['documentDescribes'].append(package_info['SPDXID']) + + self._process_license_refs(lic_refs, spdx_document) + + def _create_package_info(self, purl: str, comp: dict, lic_refs: set) -> dict: + """ + Create package information for SPDX document. + + This method generates a complete package information entry following the SPDX + specification format. It creates a unique identifier for the package based on + its PURL and version, processes license information, and formats all required + fields for the SPDX document. + + Args: + purl (str): Package URL identifier for the component + comp (dict): Component information dictionary containing metadata like + component name, version, URLs, and license information + lic_refs (set): Reference to a set that will be populated with license + references found in this package. This set is modified in place. + + Returns: + dict: A dictionary containing all required SPDX package fields including: + - name: Component name + - SPDXID: Unique identifier for this package within the document + - versionInfo: Component version + - downloadLocation: URL where the package can be downloaded + - homepage: Component homepage URL + - licenseDeclared: Formatted license expression + - licenseConcluded: NOASSERTION as automated conclusion isn't possible + - filesAnalyzed: False as files are not individually analyzed + - copyrightText: NOASSERTION as copyright text isn't available + - supplier: Organization name from vendor information + - externalRefs: Package URL reference for package manager integration + - checksums: MD5 hash of the package if available + """ + lic_text = self._process_package_licenses(comp.get('licenses', []), lic_refs) + comp_ver = comp.get('version') + purl_ver = f'{purl}@{comp_ver}' + purl_hash = hashlib.md5(purl_ver.encode('utf-8')).hexdigest() + + external_refs = [ + { + 'referenceCategory': 'PACKAGE-MANAGER', + 'referenceLocator': PackageURL.from_string(purl_ver).to_string(), + 'referenceType': 'purl' + } + ] + external_refs.extend(self._create_cpe_external_refs(comp.get('cpes', []))) + + return { + 'name': comp.get('component'), + 'SPDXID': f'SPDXRef-{purl_hash}', + 'versionInfo': comp_ver, + 'downloadLocation': comp.get('download_url') or comp.get('url'), + 'homepage': comp.get('url', ''), + 'licenseDeclared': lic_text, + 'licenseConcluded': 'NOASSERTION', + 'filesAnalyzed': False, + 'copyrightText': 'NOASSERTION', + 'supplier': f'Organization: {comp.get("vendor", "NOASSERTION")}', + 'externalRefs': external_refs, + 'checksums': [ + { + 'algorithm': 'MD5', + 'checksumValue': comp.get('url_hash') or '0' * 32 + } + ], + } + + def _create_cpe_external_refs(self, cpes: list) -> list: + """ + Build SPDX externalRefs entries for a component's CPE identifiers. + + SPDX 2.2 models CPEs under the SECURITY reference category. Each CPE string + must be emitted as its own externalRef dict with the shape: + + { + 'referenceCategory': 'SECURITY', + 'referenceType': 'cpe23Type' | 'cpe22Type', + 'referenceLocator': '', + } + + Args: + cpes (list): CPE strings extracted from the raw scan results. The list is + already deduplicated by `_extract_cpes`. Values look like + 'cpe:2.3:a:vendor:product:version:...' (CPE 2.3) or + 'cpe:/a:vendor:product:version' (legacy CPE 2.2). May be empty. + + Returns: + list: A list of SPDX externalRef dicts ready to be appended to a package's + `externalRefs`. Return an empty list when `cpes` is empty. + """ + if not cpes: + return [] + refs = [] + for cpe in cpes: + normalized = cpe.lower() + if normalized.startswith('cpe:2.3:'): + ref_type = 'cpe23Type' + elif normalized.startswith('cpe:/') or normalized.startswith('cpe:2.2:'): + ref_type = 'cpe22Type' + else: + self.print_debug(f'Warning: Unrecognized CPE format, defaulting to cpe23Type: {cpe}') + ref_type = 'cpe23Type' + refs.append({ + 'referenceCategory': 'SECURITY', + 'referenceType': ref_type, + 'referenceLocator': cpe, }) - # End for loop - file = sys.stdout + return refs + + def _process_package_licenses(self, licenses: list, lic_refs: set) -> str: + """ + Process licenses and return license text formatted for SPDX. + + This method processes a list of license objects, extracts valid license IDs, + converts them to SPDX format, and combines them into a properly formatted + license expression. + + Args: + licenses (list): List of license dictionaries, each containing at least + an 'id' field + lic_refs (set): Reference to a set that will collect license references. + This set is modified in place. + + Returns: + str: A formatted license expression string following SPDX syntax. + Returns 'NOASSERTION' if no valid licenses are found. + """ + if not licenses: + return 'NOASSERTION' + + lic_set = set() + for lic in licenses: + lc_id = lic.get('id') + self._process_license_id(lc_id, lic_refs, lic_set) + + return self._format_license_text(lic_set) + + def _process_license_id(self, lc_id: str, lic_refs: set, lic_set: set): + """ + Process individual license ID and add to appropriate sets. + + This method attempts to convert a license ID to its SPDX equivalent. + If not found in the SPDX license list, it's formatted as a LicenseRef + and added to the license references set. + + Args: + lc_id (str): The license ID to process + lic_refs (set): Reference to a set that collects license references + for later processing. Modified in place. + lic_set (set): Reference to a set collecting all license IDs for + """ + spdx_id = self.get_spdx_license_id(lc_id) + if not spdx_id: + if not lc_id.startswith('LicenseRef'): + lc_id = f'LicenseRef-{lc_id}' + lic_refs.add(lc_id) + lic_set.add(spdx_id if spdx_id else lc_id) + + def _format_license_text(self, lic_set: set) -> str: + """ + Format the license text with proper SPDX syntax. + + This method combines multiple license IDs with the 'AND' operator + according to SPDX specification rules. If multiple licenses are present, + the expression is enclosed in parentheses. + + Args: + lic_set (set): Set of license IDs to format + + Returns: + str: A properly formatted SPDX license expression. + Returns 'NOASSERTION' if the set is empty. + """ + if not lic_set: + return 'NOASSERTION' + + lic_text = ' AND '.join(lic_set) + if len(lic_set) > 1: + lic_text = f'({lic_text})' + return lic_text + + def _process_license_refs(self, lic_refs: set, spdx_document: dict): + """ + Process and add license references to the SPDX document. + + This method processes each license reference in the provided set + and adds corresponding license information to the SPDX document's + extracted licensing information section. + + Args: + lic_refs (set): Set of license references to process + spdx_document (dict): Reference to the SPDX document being built, + which will be modified in place + + Note: + This method modifies the spdx_document dictionary in place by adding + entries to the 'hasExtractedLicensingInfos' list. + """ + for lic_ref in lic_refs: + license_info = self._parse_license_ref(lic_ref) + spdx_document['hasExtractedLicensingInfos'].append(license_info) + + def _parse_license_ref(self, lic_ref: str) -> dict: + """ + Parse license reference and create info dictionary for SPDX document. + + This method extracts information from a license reference identifier + and formats it into the structure required by the SPDX specification + for extracted licensing information. + + Args: + lic_ref (str): License reference identifier to parse + + Returns: + dict: Dictionary containing required SPDX fields for extracted license info: + - licenseId: The unique identifier for this license + - name: A readable name for the license + - extractedText: A placeholder for the actual license text + - comment: Information about how the license was detected + """ + source, name = self._extract_license_info(lic_ref) + source_text = f' by {source}.' if source else '.' + + return { + 'licenseId': lic_ref, + 'name': name.replace('-', ' '), + 'extractedText': 'Detected license, please review component source code.', + 'comment': f'Detected license{source_text}', + } + + def _extract_license_info(self, lic_ref: str): + """ + Extract source and name from license reference. + + This method parses a license reference string to extract the source + (e.g., scancode, scanoss) and the actual license name using regular + expressions. + + Args: + lic_ref (str): License reference identifier to parse + + Returns: + tuple: A tuple containing (source, name) where: + - source (str): The tool or system that identified the license + - name (str): The actual license name + """ + match = re.search(r'^LicenseRef-(scancode-|scanoss-|)(\S+)$', lic_ref, re.IGNORECASE) + if match: + source = match.group(1).replace('-', '') + name = match.group(2) + else: + source = '' + name = lic_ref + return source, name + + def _write_output(self, data: dict, output_file: str = None) -> bool: + """Write the SPDX document to output.""" + try: + file = self._get_output_file(output_file) + print(json.dumps(data, indent=2), file=file) + if output_file: + file.close() + return True + except Exception as e: + self.print_stderr(f'Error writing output: {str(e)}') + return False + + def _get_output_file(self, output_file: str = None): + """Get the appropriate output file handle.""" if not output_file and self.output_file: output_file = self.output_file - if output_file: - file = open(output_file, 'w') - print(json.dumps(data, indent=2), file=file) - if output_file: - file.close() - return True + return open(output_file, 'w') if output_file else sys.stdout def produce_from_str(self, json_str: str, output_file: str = None) -> bool: """ @@ -255,6 +716,8 @@ def load_license_data(self) -> None: Load the embedded SPDX valid license JSON files Parse its contents to provide a lookup for valid name """ + # SPDX license files details from: https://spdx.org/licenses/ + # Specifically the JSON files come from GitHub: https://github.com/spdx/license-list-data/tree/master/json self._spdx_licenses = {} self._spdx_lic_names = {} self.print_debug('Loading SPDX License details...') @@ -270,9 +733,10 @@ def load_license_data_file(self, filename: str, lic_field: str = 'licenseId') -> :return: True if successful, False otherwise """ try: - f_name = pkg_resources.resource_filename(__name__, filename) - with open(f_name, 'r') as f: - data = json.loads(f.read()) + f_name = importlib_resources.files(__name__) / filename + with importlib_resources.as_file(f_name) as f: + with open(f, 'r', encoding='utf-8') as file: + data = json.load(file) except Exception as e: self.print_stderr(f'ERROR: Problem parsing SPDX license input JSON: {e}') return False @@ -290,8 +754,6 @@ def load_license_data_file(self, filename: str, lic_field: str = 'licenseId') -> self._spdx_licenses[lic_id_short] = lic_id if lic_name: self._spdx_lic_names[lic_name] = lic_id - # self.print_stderr(f'Licenses: {self._spdx_licenses}') - # self.print_stderr(f'Lookup: {self._spdx_lic_lookup}') return True def get_spdx_license_id(self, lic_name: str) -> str: @@ -316,8 +778,10 @@ def get_spdx_license_id(self, lic_name: str) -> str: lic_id = self._spdx_lic_names.get(search_name_dashes) if lic_id: return lic_id - self.print_stderr(f'Warning: Failed to find valid SPDX license identifier for: {lic_name}') + self.print_debug(f'Warning: Failed to find valid SPDX license identifier for: {lic_name}') return None + + # # End of SpdxLite Class # diff --git a/src/scanoss/threadeddependencies.py b/src/scanoss/threadeddependencies.py index c6ca12b7..9fcd4395 100644 --- a/src/scanoss/threadeddependencies.py +++ b/src/scanoss/threadeddependencies.py @@ -1,50 +1,78 @@ """ - SPDX-License-Identifier: MIT - - Copyright (c) 2021, SCANOSS - - 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. +SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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. """ -import threading +import json import queue -from typing import Dict +import threading from dataclasses import dataclass +from enum import Enum +from typing import Dict from .scancodedeps import ScancodeDeps from .scanossbase import ScanossBase from .scanossgrpc import ScanossGrpc +DEP_FILE_PREFIX = 'file=' # Default prefix to signify an existing parsed dependency file + +DEV_DEPENDENCIES = { + 'dev', + 'test', + 'development', + 'provided', + 'runtime', + 'devDependencies', + 'dev-dependencies', + 'testImplementation', + 'testCompile', + 'Test', + 'require-dev', +} + + +# Define an enum class +class SCOPE(Enum): + PRODUCTION = 'prod' + DEVELOPMENT = 'dev' + @dataclass class ThreadedDependencies(ScanossBase): - """ + """ """ - """ inputs: queue.Queue = queue.Queue() output: queue.Queue = queue.Queue() - def __init__(self, sc_deps: ScancodeDeps, grpc_api: ScanossGrpc, what_to_scan: str = None, debug: bool = False, - trace: bool = False, quiet: bool = False) -> None: - """ - - """ + def __init__( + self, + sc_deps: ScancodeDeps, + grpc_api: ScanossGrpc, + what_to_scan: str = None, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + ) -> None: + """ """ super().__init__(debug, trace, quiet) self.sc_deps = sc_deps self.grpc_api = grpc_api @@ -64,45 +92,133 @@ def responses(self) -> Dict: return resp return None - def run(self, what_to_scan: str = None, wait: bool = True) -> bool: + def run( + self, + what_to_scan: str = None, + deps_file: str = None, + wait: bool = True, + dep_scope: SCOPE = None, + dep_scope_include: str = None, + dep_scope_exclude: str = None, + ) -> bool: """ Initiate a background scan for the specified file/dir + :param dep_scope_exclude: comma separated list of dependency scopes to exclude + :param dep_scope_include: comma separated list of dependency scopes to include + :param dep_scope: Enum dependency scope to use :param what_to_scan: file/folder to scan + :param deps_file: file to decorate instead of scan (overrides what_to_scan option) :param wait: wait for completion :return: True if successful, False if error encountered """ what_to_scan = what_to_scan if what_to_scan else self.what_to_scan self._errors = False try: - self.print_msg(f'Searching {what_to_scan} for dependencies...') - self.inputs.put(what_to_scan) # Set up an input queue to enable the parent to wait for completion - self._thread = threading.Thread(target=self.scan_dependencies, daemon=True) + if deps_file: # Decorate the given dependencies file + self.print_msg(f'Decorating {deps_file} dependencies...') + self.inputs.put(f'{DEP_FILE_PREFIX}{deps_file}') # Add to queue and have parent wait on it + else: # Search for dependencies to decorate + self.print_msg(f'Searching {what_to_scan} for dependencies...') + self.inputs.put(what_to_scan) + # Add to queue and have parent wait on it + self._thread = threading.Thread( + target=self.scan_dependencies(dep_scope, dep_scope_include, dep_scope_exclude), daemon=True + ) self._thread.start() except Exception as e: self.print_stderr(f'ERROR: Problem running threaded dependencies: {e}') self._errors = True - if wait and not self._errors: # Wait for all inputs to complete + if wait and not self._errors: # Wait for all inputs to complete self.complete() return False if self._errors else True - def scan_dependencies(self) -> None: + def filter_dependencies(self, deps, filter_dep) -> json: + files = deps.get('files', []) + # Iterate over files and their purls + for file in files: + if 'purls' in file: + # Filter purls with scope 'dependencies' and remove the scope field + file['purls'] = [ + {key: value for key, value in purl.items() if key != 'scope'} + for purl in file['purls'] + if filter_dep(purl.get('scope')) + ] + # End of for loop + + return {'files': [file for file in deps.get('files', []) if file.get('purls')]} + + def filter_dependencies_by_scopes( + self, deps: json, dep_scope: SCOPE = None, dep_scope_include: str = None, dep_scope_exclude: str = None + ) -> json: + # Predefined set of scopes to filter + + # Include all scopes + include_all = (dep_scope is None or dep_scope == '') and dep_scope_include is None and dep_scope_exclude is None + ## All dependencies, remove scope key + if include_all: + return self.filter_dependencies(deps, lambda purl: True) + + # Use default list of scopes if a custom list is not set + if (dep_scope is not None and dep_scope != '') and dep_scope_include is None and dep_scope_exclude is None: + return self.filter_dependencies( + deps, + lambda purl: (dep_scope == SCOPE.PRODUCTION and purl not in DEV_DEPENDENCIES) + or dep_scope == SCOPE.DEVELOPMENT + and purl in DEV_DEPENDENCIES, + ) + + if ( + (dep_scope_include is not None and dep_scope_include != '') + or dep_scope_exclude is not None + and dep_scope_exclude != '' + ): + # Create sets from comma-separated strings, if provided + exclude = set(dep_scope_exclude.split(',')) if dep_scope_exclude else set() + include = set(dep_scope_include.split(',')) if dep_scope_include else set() + + # Define a lambda function that checks the inclusion/exclusion logic + return self.filter_dependencies( + deps, lambda purl: (exclude and purl not in exclude) or (not exclude and purl in include) + ) + return None + + def scan_dependencies( # noqa: PLR0912 + self, dep_scope: SCOPE = None, dep_scope_include: str = None, dep_scope_exclude: str = None + ) -> None: """ - Scan for dependencies from the given file/dir (from the input queue) + Scan for dependencies from the given file/dir or from an input file (from the input queue). """ + # TODO refactor to simplify branches based on PLR0912 current_thread = threading.get_ident() self.print_trace(f'Starting dependency worker {current_thread}...') try: - what_to_scan = self.inputs.get(timeout=5) # Begin processing the dependency request - if not self.sc_deps.run_scan(what_to_scan=what_to_scan): + what_to_scan = self.inputs.get(timeout=5) # Begin processing the dependency request + deps = None + if what_to_scan.startswith(DEP_FILE_PREFIX): # We have a pre-parsed dependency file, load it + deps = self.sc_deps.load_from_file(what_to_scan.strip(DEP_FILE_PREFIX)) + if deps: + deps = self.sc_deps.filter_dependencies_by_path(deps) + elif not self.sc_deps.run_scan(what_to_scan=what_to_scan): self._errors = True else: deps = self.sc_deps.produce_from_file() + if deps: + deps = self.sc_deps.filter_dependencies_by_path(deps) + if dep_scope is not None: + self.print_debug(f'Filtering {dep_scope.name} dependencies') + if dep_scope_include is not None: + self.print_debug(f"Including dependencies with '{dep_scope_include.split(',')}' scopes") + if dep_scope_exclude is not None: + self.print_debug(f"Excluding dependencies with '{dep_scope_exclude.split(',')}' scopes") + deps = self.filter_dependencies_by_scopes(deps, dep_scope, dep_scope_include, dep_scope_exclude) + + if not self._errors: if deps is None: self.print_stderr(f'Problem searching for dependencies for: {what_to_scan}') self._errors = True - elif not deps: - self.print_trace(f'No dependencies found to decorate for: {what_to_scan}') - else: # TODO add API call to get dep data + elif not deps or len(deps.get('files', [])) == 0: + self.print_debug(f'No dependencies found to decorate for: {what_to_scan}') + else: decorated_deps = self.grpc_api.get_dependencies(deps) if decorated_deps: self.output.put(decorated_deps) @@ -128,6 +244,7 @@ def complete(self) -> bool: self._errors = True return True if not self._errors else False + # # End of ThreadedDependencies Class # diff --git a/src/scanoss/threadedscanning.py b/src/scanoss/threadedscanning.py index 20e21cba..596db599 100644 --- a/src/scanoss/threadedscanning.py +++ b/src/scanoss/threadedscanning.py @@ -1,41 +1,46 @@ """ - SPDX-License-Identifier: MIT - - Copyright (c) 2021, SCANOSS - - 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. +SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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. """ + +import atexit import os +import queue import sys import threading -import queue import time - -from typing import Dict, List from dataclasses import dataclass +from typing import Dict, List + from progress.bar import Bar from .scanossapi import ScanossApi from .scanossbase import ScanossBase -WFP_FILE_START = "file=" -MAX_ALLOWED_THREADS = 30 +WFP_FILE_START = 'file=' +MAX_ALLOWED_THREADS = ( + int(os.environ.get('SCANOSS_MAX_ALLOWED_THREADS')) if os.environ.get('SCANOSS_MAX_ALLOWED_THREADS') else 30 +) + @dataclass class ThreadedScanning(ScanossBase): @@ -44,13 +49,12 @@ class ThreadedScanning(ScanossBase): WFP scan requests are loaded into the input queue. Multiple threads pull messages off this queue, process the request and put the results into an output queue """ - inputs: queue.Queue = queue.Queue() - output: queue.Queue = queue.Queue() + bar: Bar = None - def __init__(self, scanapi :ScanossApi, debug: bool = False, trace: bool = False, quiet: bool = False, - nb_threads: int = 5 - ) -> None: + def __init__( + self, scanapi: ScanossApi, debug: bool = False, trace: bool = False, quiet: bool = False, nb_threads: int = 5 + ) -> None: """ Initialise the ThreadedScanning class :param scanapi: SCANOSS API to send scan requests to @@ -60,17 +64,22 @@ def __init__(self, scanapi :ScanossApi, debug: bool = False, trace: bool = False :param nb_threads: Number of thread to run (default 5) """ super().__init__(debug, trace, quiet) + self.inputs = queue.Queue() + self.output = queue.Queue() self.scanapi = scanapi self.nb_threads = nb_threads self._isatty = sys.stderr.isatty() self._bar_count = 0 self._errors = False self._lock = threading.Lock() - self._stop_event = threading.Event() + self._stop_event = threading.Event() # Control when scanning threads should terminate + self._stop_scanning = threading.Event() # Control if the parent process should abort scanning self._threads = [] if nb_threads > MAX_ALLOWED_THREADS: self.print_msg(f'Warning: Requested threads too large: {nb_threads}. Reducing to {MAX_ALLOWED_THREADS}') self.nb_threads = MAX_ALLOWED_THREADS + # Register cleanup to ensure progress bar is finished on exit + atexit.register(self.complete_bar) @staticmethod def __count_files_in_wfp(wfp: str): @@ -95,6 +104,13 @@ def complete_bar(self): if self.bar: self.bar.finish() + def __del__(self): + """Ensure progress bar is cleaned up when object is destroyed""" + try: + self.complete_bar() + except Exception: + pass # Ignore errors during cleanup + def set_bar(self, bar: Bar) -> None: """ Set the Progress Bar to display progress while scanning @@ -106,6 +122,8 @@ def update_bar(self, amount: int = 0, create: bool = False, file_count: int = 0) """ Update the Progress Bar progress :param amount: amount of progress to update + :param create: create the bar if requested + :param file_count: file count """ try: self._lock.acquire() @@ -120,16 +138,29 @@ def update_bar(self, amount: int = 0, create: bool = False, file_count: int = 0) except Exception as e: self.print_debug(f'Warning: Update status bar lock failed: {e}. Ignoring.') - def queue_add(self, wfp: str) -> None: + def queue_add(self, wfp: str, sbom: dict = None) -> None: """ Add requests to the queue :param wfp: WFP to add to queue + :param sbom: Per-request SBOM context (optional, overrides global SBOM) """ - self.inputs.put(wfp) + if sbom and self.debug: + w = (wfp.split('\n', maxsplit=1))[0] # show the first file to help debug context. + self.print_debug(f'Adding SBOM context: {sbom} to {w} ...') + if wfp is None or wfp == '': + self.print_stderr('Warning: empty WFP. Skipping from scan...') + else: + self.inputs.put((wfp, sbom)) def get_queue_size(self) -> int: return self.inputs.qsize() + def stop_scanning(self) -> bool: + """ + Check if we should keep scanning or not + """ + return self._stop_scanning.is_set() + @property def responses(self) -> List[Dict]: """ @@ -145,8 +176,9 @@ def run(self, wait: bool = True) -> bool: """ qsize = self.inputs.qsize() if qsize < self.nb_threads: - self.print_debug(f'Input queue ({qsize}) smaller than requested threads: {self.nb_threads}. ' - f'Reducing to queue size.') + self.print_debug( + f'Input queue ({qsize}) smaller than requested threads: {self.nb_threads}. Reducing to queue size.' + ) self.nb_threads = qsize else: self.print_debug(f'Starting {self.nb_threads} threads to process {qsize} requests...') @@ -158,7 +190,7 @@ def run(self, wait: bool = True) -> bool: except Exception as e: self.print_stderr(f'ERROR: Problem running threaded scanning: {e}') self._errors = True - if wait: # Wait for all inputs to complete + if wait: # Wait for all inputs to complete self.complete() return False if self._errors else True @@ -167,7 +199,7 @@ def complete(self) -> bool: Wait for input queue to complete processing and complete the worker threads """ self.inputs.join() - self._stop_event.set() # Tell the worker threads to stop + self._stop_event.set() # Tell the worker threads to stop try: for t in self._threads: # Complete the threads t.join(timeout=5) @@ -183,30 +215,39 @@ def worker_post(self) -> None: """ current_thread = threading.get_ident() self.print_trace(f'Starting worker {current_thread}...') + api_error = False while not self._stop_event.is_set(): wfp = None - if not self.inputs.empty(): # Only try to get a message if there is one on the queue + if not self.inputs.empty(): # Only try to get a message if there is one on the queue try: - wfp = self.inputs.get(timeout=5) - self.print_trace(f'Processing input request ({current_thread})...') - count = self.__count_files_in_wfp(wfp) - resp = self.scanapi.scan(wfp, scan_id=current_thread) - if resp: - self.output.put(resp) # Store the output response to later collection - self.update_bar(count) - self.inputs.task_done() - self.print_trace(f'Request complete ({current_thread}).') - except queue.Empty as e: + wfp, sbom = self.inputs.get(timeout=5) + if api_error: # API error encountered, so stop processing anymore requests + self.inputs.task_done() # remove request from the queue + else: + self.print_trace(f'Processing input request ({current_thread})...') + count = self.__count_files_in_wfp(wfp) + if wfp is None or wfp == '': + self.print_stderr(f'Warning: Empty WFP in request input: {wfp}') + resp = self.scanapi.scan(wfp, scan_id=current_thread, sbom=sbom) + if resp: + self.output.put(resp) # Store the output response to later collection + self.update_bar(count) + self.inputs.task_done() + self.print_trace(f'Request complete ({current_thread}).') + except queue.Empty: self.print_stderr(f'No message available to process ({current_thread}). Checking again...') except Exception as e: - ThreadedScanning.print_stderr(f'ERROR: Problem encountered running scan: {e}') + self.print_stderr(f'ERROR: Problem encountered running scan: {e}. Aborting current thread.') self._errors = True if wfp: self.inputs.task_done() # If there was a WFP being processed, remove it from the queue + api_error = True # Stop processing anymore work requests + self._stop_scanning.set() # Tell the parent process to abort scanning else: time.sleep(1) # Sleep while waiting for the queue depth to build up self.print_trace(f'Thread complete ({current_thread}).') + # # End of ThreadedScanning Class -# \ No newline at end of file +# diff --git a/src/scanoss/utils/__init__.py b/src/scanoss/utils/__init__.py new file mode 100644 index 00000000..ebd5917f --- /dev/null +++ b/src/scanoss/utils/__init__.py @@ -0,0 +1,23 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2024, SCANOSS + + 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/src/scanoss/utils/abstract_presenter.py b/src/scanoss/utils/abstract_presenter.py new file mode 100644 index 00000000..ba6073db --- /dev/null +++ b/src/scanoss/utils/abstract_presenter.py @@ -0,0 +1,103 @@ +from abc import ABC, abstractmethod + +from scanoss.scanossbase import ScanossBase + + +class AbstractPresenter(ABC): + """ + Abstract presenter class for presenting output in a given format. + Subclasses must implement the _format_json_output and _format_plain_output methods. + """ + + def __init__( + self, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + output_file: str = None, + output_format: str = None, + ): + """ + Initialize the presenter with the given output file and format. + """ + self.AVAILABLE_OUTPUT_FORMATS = ['json', 'plain', 'cyclonedx', 'spdxlite', 'csv', 'raw'] + self.base = ScanossBase(debug=debug, trace=trace, quiet=quiet) + self.output_file = output_file + self.output_format = output_format + + def present(self, output_format: str = None, output_file: str = None): + """ + Present the formatted output to a file if provided; otherwise, print to stdout. + """ + file_path = output_file or self.output_file + fmt = output_format or self.output_format + + if fmt and fmt not in self.AVAILABLE_OUTPUT_FORMATS: + raise ValueError( + f"ERROR: Invalid output format '{fmt}'. Valid values are: {', '.join(self.AVAILABLE_OUTPUT_FORMATS)}" + ) + + if fmt == 'json': + content = self._format_json_output() + elif fmt == 'plain': + content = self._format_plain_output() + elif fmt == 'cyclonedx': + content = self._format_cyclonedx_output() + elif fmt == 'spdxlite': + content = self._format_spdxlite_output() + elif fmt == 'csv': + content = self._format_csv_output() + elif fmt == 'raw': + content = self._format_raw_output() + else: + content = self._format_plain_output() + + self._present_output(content, file_path) + + def _present_output(self, content: str, file_path: str = None): + """ + If a file path is provided, write to that file; otherwise, print the content to stdout. + """ + self.base.print_to_file_or_stdout(content, file_path) + + @abstractmethod + def _format_cyclonedx_output(self) -> str: + """ + Return a CycloneDX string representation of the data. + """ + pass + + @abstractmethod + def _format_spdxlite_output(self) -> str: + """ + Return a SPDX-Lite string representation of the data. + """ + pass + + @abstractmethod + def _format_csv_output(self) -> str: + """ + Return a CSV string representation of the data. + """ + pass + + @abstractmethod + def _format_json_output(self) -> str: + """ + Return a JSON string representation of the data. + """ + pass + + @abstractmethod + def _format_plain_output(self) -> str: + """ + Return a plain text string representation of the data. + """ + pass + + @abstractmethod + def _format_raw_output(self) -> str: + """ + Return a raw string representation of the data. + """ + pass diff --git a/src/scanoss/utils/crc64.py b/src/scanoss/utils/crc64.py new file mode 100644 index 00000000..d39785d6 --- /dev/null +++ b/src/scanoss/utils/crc64.py @@ -0,0 +1,96 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +import struct +from typing import List + + +class CRC64: + """ + CRC64 ECMA implementation matching Go's hash/crc64 package. + Uses polynomial: 0xC96C5795D7870F42 + """ + + POLY = 0xC96C5795D7870F42 + _TABLE = None + + def __init__(self): + if CRC64._TABLE is None: + CRC64._TABLE = self._make_table() + self.crc = 0xFFFFFFFFFFFFFFFF # Initial value + + def _make_table(self) -> list: + """Generate the CRC64 lookup table.""" + table = [] + for i in range(256): + crc = i + for _ in range(8): + if crc & 1: + crc = (crc >> 1) ^ self.POLY + else: + crc >>= 1 + table.append(crc) + return table + + def update(self, data: bytes) -> None: + """Update the CRC with new data.""" + if isinstance(data, str): + data = data.encode('utf-8') + + crc = self.crc + for b in data: + crc = (crc >> 8) ^ CRC64._TABLE[(crc ^ b) & 0xFF] # Use class-level table + self.crc = crc + + def digest(self) -> int: + """Get the current CRC value.""" + return self.crc ^ 0xFFFFFFFFFFFFFFFF # Final XOR value + + def hexdigest(self): + """Get the current CRC value as a hexadecimal string.""" + return format(self.digest(), '016x') + + @classmethod + def checksum(cls, data: bytes) -> int: + """Calculate CRC64 checksum for the given data.""" + crc = cls() + crc.update(data) + return crc.digest() + + @classmethod + def get_hash_buff(cls, buff: bytes) -> List[bytes]: + """ + Get the hash value of the given buffer, and converts it to 8 bytes in big-endian order. + + Args: + buff (bytes): The buffer to get the hash value of. + + Returns: + bytes: The hash value of the given buffer, and converts it to 8 bytes in big-endian order. + """ + crc = cls() + crc.update(buff) + hash_val = crc.digest() + + return list(struct.pack('>Q', hash_val)) diff --git a/src/scanoss/utils/file.py b/src/scanoss/utils/file.py new file mode 100644 index 00000000..84c6dc47 --- /dev/null +++ b/src/scanoss/utils/file.py @@ -0,0 +1,84 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2024, SCANOSS + + 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. +""" + +import json +import os +from dataclasses import dataclass +from typing import Optional + +JSON_ERROR_PARSE = 1 +JSON_ERROR_FILE_NOT_FOUND = 2 +JSON_ERROR_FILE_EMPTY = 3 +JSON_ERROR_FILE_SIZE = 4 + + +@dataclass +class JsonValidation: + is_valid: bool + data: Optional[dict] = None + error: Optional[str] = None + error_code: Optional[int] = None + + +def validate_json_file(json_file_path: str) -> JsonValidation: + """ + Validate if the specified file is indeed a valid JSON file + + Args: + json_file_path (str): The JSON file to validate + + Returns: + JsonValidation: A JsonValidation object containing a boolean indicating if the file is valid, the data, error, and error code + """ # noqa: E501 + if not json_file_path: + return JsonValidation(is_valid=False, error='No JSON file specified') + if not os.path.isfile(json_file_path): + return JsonValidation( + is_valid=False, + error=f'File not found: {json_file_path}', + error_code=JSON_ERROR_FILE_NOT_FOUND, + ) + try: + if os.stat(json_file_path).st_size == 0: + return JsonValidation( + is_valid=False, + error=f'File is empty: {json_file_path}', + error_code=JSON_ERROR_FILE_EMPTY, + ) + except OSError as e: + return JsonValidation( + is_valid=False, + error=f'Problem checking file size: {json_file_path}: {e}', + error_code=JSON_ERROR_FILE_SIZE, + ) + try: + with open(json_file_path) as f: + data = json.load(f) + return JsonValidation(is_valid=True, data=data) + except json.JSONDecodeError as e: + return JsonValidation( + is_valid=False, + error=f'Problem parsing JSON file: "{json_file_path}": {e}', + error_code=JSON_ERROR_PARSE, + ) diff --git a/src/scanoss/utils/scanoss_scan_results_utils.py b/src/scanoss/utils/scanoss_scan_results_utils.py new file mode 100644 index 00000000..a9ac1fbb --- /dev/null +++ b/src/scanoss/utils/scanoss_scan_results_utils.py @@ -0,0 +1,41 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +def get_lines(lines: str) -> list: + """ + Parse line range string into a list of line numbers. + + Converts SCANOSS line notation (e.g., '10-20,25-30') into a flat list + of individual line numbers for processing. + + :param lines: Comma-separated line ranges in SCANOSS format (e.g., '10-20,25-30') + :return: Flat list of all line numbers extracted from the ranges + """ + lines_list = [] + lines = lines.split(',') + for line in lines: + line_parts = line.split('-') + for part in line_parts: + lines_list.append(int(part)) + return lines_list \ No newline at end of file diff --git a/src/scanoss/utils/simhash.py b/src/scanoss/utils/simhash.py new file mode 100644 index 00000000..1ed56886 --- /dev/null +++ b/src/scanoss/utils/simhash.py @@ -0,0 +1,198 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +import re +import unicodedata + +FNV64_OFFSET_BASIS = 14695981039346656037 +FNV64_PRIME = 1099511628211 +MASK64 = 0xFFFFFFFFFFFFFFFF + + +def fnv1_64(data: bytes) -> int: + """Compute the 64‐bit FNV‑1 hash of data.""" + h = FNV64_OFFSET_BASIS + for b in data: + h = (h * FNV64_PRIME) & MASK64 + h = h ^ b + return h + + +class SimhashFeature: + def __init__(self, hash_value: int, weight: int = 1): + self.hash_value = hash_value + self.weight = weight + + def sum(self) -> int: + """Return the 64-bit hash (sum) of this feature.""" + return self.hash_value + + def get_weight(self) -> int: + """Return the weight of this feature.""" + return self.weight + + +def new_feature(f: bytes) -> SimhashFeature: + """Return a new feature for the given byte slice with weight 1.""" + return SimhashFeature(fnv1_64(f), 1) + + +def new_feature_with_weight(f: bytes, weight: int) -> SimhashFeature: + """Return a new feature for the given byte slice with the given weight.""" + return SimhashFeature(fnv1_64(f), weight) + + +def vectorize(features: list) -> list: + """ + Given a list of features, return a 64-element vector. + Each feature contributes its weight to each coordinate, + added if that bit is set and subtracted otherwise. + """ + v = [0] * 64 + for feature in features: + h = feature.sum() + w = feature.get_weight() + for i in range(64): + if ((h >> i) & 1) == 1: + v[i] += w + else: + v[i] -= w + return v + + +def vectorize_bytes(features: list) -> list: + """ + Given a list of byte slices, treat each as a feature (with weight 1) + by computing its FNV-1 hash. + """ + v = [0] * 64 + for feat in features: + h = fnv1_64(feat) + for i in range(64): + if ((h >> i) & 1) == 1: + v[i] += 1 + else: + v[i] -= 1 + return v + + +def fingerprint(v: list) -> int: + """ + Given a 64-element vector, return a 64-bit fingerprint. + For each bit i, if v[i] >= 0, set bit i to 1; otherwise leave it 0. + """ + f = 0 + for i in range(64): + if v[i] >= 0: + f |= 1 << i + return f + + +def compare(a: int, b: int) -> int: + """ + Calculate the Hamming distance between two 64-bit integers. + (The number of differing bits.) + """ + v = a ^ b + c = 0 + while v: + v &= v - 1 + c += 1 + return c + + +def simhash(fs) -> int: + """ + Given a feature set (an object with a get_features() method), + return its 64-bit simhash. + """ + return fingerprint(vectorize(fs.get_features())) + + +def simhash_bytes(b: list) -> int: + """ + Given a list of byte slices, return the simhash. + """ + return fingerprint(vectorize_bytes(b)) + + +boundaries = re.compile(rb"[\w']+(?:\://[\w\./]+){0,1}") +unicode_boundaries = re.compile(r"[\w'-]+", re.UNICODE) + + +# --- Helper Functions for Feature Extraction --- +def _get_features_bytes(b: bytes, pattern: re.Pattern) -> list: + """ + Split the given byte string using the given regex pattern, + and return a list of features (each created with new_feature). + """ + words = pattern.findall(b) + return [new_feature(word) for word in words] + + +def _get_features_str(s: str, pattern) -> list: + """ + Split the given string using the given regex pattern, + and return a list of features (each created by encoding to UTF-8). + """ + words = pattern.findall(s) + return [new_feature(word.encode('utf-8')) for word in words] + + +class WordFeatureSet: + def __init__(self, b: bytes): + # Normalize the input to lowercase. + self.b = b.lower() + + def get_features(self) -> list: + return _get_features_bytes(self.b, boundaries) + + +class UnicodeWordFeatureSet: + def __init__(self, b: bytes, norm_form: str = 'NFC'): + # Decode, normalize (using the provided form), and lowercase. + text = b.decode('utf-8') + normalized = unicodedata.normalize(norm_form, text) + self.text = normalized.lower() + + def get_features(self) -> list: + return _get_features_str(self.text, unicode_boundaries) + + +def shingle(w: int, b: list) -> list: + """ + Return the w-shingling of the given set of byte slices. + For example, if b is [b"this", b"is", b"a", b"test"] + and w == 2, the result is [b"this is", b"is a", b"a test"]. + """ + if w < 1: + raise ValueError('simhash.shingle(): k must be a positive integer') + if w == 1: + return b + w = min(w, len(b)) + count = len(b) - w + 1 + shingles = [] + for i in range(count): + shingles.append(b' '.join(b[i : i + w])) + return shingles diff --git a/src/scanoss/winnowing.py b/src/scanoss/winnowing.py index cc0e58d7..6af7f06f 100644 --- a/src/scanoss/winnowing.py +++ b/src/scanoss/winnowing.py @@ -1,38 +1,43 @@ """ - SPDX-License-Identifier: MIT +SPDX-License-Identifier: MIT - Copyright (c) 2021, SCANOSS + Copyright (c) 2021, SCANOSS - 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: + 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 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. + 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. - Winnowing Algorithm implementation for SCANOSS. + Winnowing Algorithm implementation for SCANOSS. - This module implements an adaptation of the original winnowing algorithm by S. Schleimer, D. S. Wilkerson and - A. Aiken as described in their seminal article which can be found here: - https://theory.stanford.edu/~aiken/publications/papers/sigmod03.pdf + This module implements an adaptation of the original winnowing algorithm by S. Schleimer, D. S. Wilkerson and + A. Aiken as described in their seminal article which can be found here: + https://theory.stanford.edu/~aiken/publications/papers/sigmod03.pdf """ + import hashlib -import sys +import pathlib +import platform +import re +from typing import Tuple -from crc32c import crc32c from binaryornot.check import is_binary +from crc32c import crc32c +from .header_filter import HeaderFilter from .scanossbase import ScanossBase # Winnowing configuration. DO NOT CHANGE. @@ -53,13 +58,63 @@ MIN_FILE_SIZE = 256 SKIP_SNIPPET_EXT = { # File extensions to ignore snippets for - ".exe", ".zip", ".tar", ".tgz", ".gz", ".7z", ".rar", ".jar", ".war", ".ear", ".class", ".pyc", - ".o", ".a", ".so", ".obj", ".dll", ".lib", ".out", ".app", ".bin", - ".lst", ".dat", ".json", ".htm", ".html", ".xml", ".md", ".txt", - ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".odt", ".ods", ".odp", ".pages", ".key", ".numbers", - ".pdf", ".min.js", ".mf", ".sum" + '.exe', + '.zip', + '.tar', + '.tgz', + '.gz', + '.7z', + '.rar', + '.jar', + '.war', + '.ear', + '.class', + '.pyc', + '.o', + '.a', + '.so', + '.obj', + '.dll', + '.lib', + '.out', + '.app', + '.bin', + '.lst', + '.dat', + '.json', + '.htm', + '.html', + '.xml', + '.md', + '.txt', + '.doc', + '.docx', + '.xls', + '.xlsx', + '.ppt', + '.pptx', + '.odt', + '.ods', + '.odp', + '.pages', + '.key', + '.numbers', + '.pdf', + '.min.js', + '.mf', + '.sum', + '.woff', + '.woff2', + '.xsd', + '.pom', + '.whl', } +CRC8_MAXIM_DOW_TABLE_SIZE = 0x100 +CRC8_MAXIM_DOW_POLYNOMIAL = 0x8C # 0x31 reflected +CRC8_MAXIM_DOW_INITIAL = 0x00 # 0x00 reflected +CRC8_MAXIM_DOW_FINAL = 0x00 # 0x00 reflected + class Winnowing(ScanossBase): """ @@ -104,21 +159,54 @@ class Winnowing(ScanossBase): a list of WFP fingerprints with their corresponding line numbers. """ - def __init__(self, size_limit: bool = True, debug: bool = False, trace: bool = False, quiet: bool = False, - skip_snippets: bool = False, post_size: int = 64, all_extensions: bool = False - ): + def __init__( # noqa: PLR0913 + self, + size_limit: bool = False, + debug: bool = False, + trace: bool = False, + quiet: bool = False, + skip_snippets: bool = False, + post_size: int = 32, + all_extensions: bool = False, + obfuscate: bool = False, + hpsm: bool = False, + strip_hpsm_ids=None, + strip_snippet_ids=None, + skip_md5_ids=None, + skip_headers: bool = False, + skip_headers_limit: int = 0, + ): """ Instantiate Winnowing class Parameters ---------- size_limit: bool - Limit the size of a fingerprint to 64k (post size) - Default True + Limit the size of a fingerprint to 32k (post size) - Default False """ super().__init__(debug, trace, quiet) - self.size_limit = size_limit + if strip_hpsm_ids is None: + strip_hpsm_ids = [] + if strip_snippet_ids is None: + strip_snippet_ids = [] + if skip_md5_ids is None: + skip_md5_ids = [] + self.size_limit = size_limit self.skip_snippets = skip_snippets self.max_post_size = post_size * 1024 if post_size > 0 else MAX_POST_SIZE self.all_extensions = all_extensions + self.obfuscate = obfuscate + self.ob_count = 1 + self.file_map = {} if obfuscate else None + self.skip_md5_ids = skip_md5_ids + self.strip_hpsm_ids = strip_hpsm_ids + self.strip_snippet_ids = strip_snippet_ids + self.hpsm = hpsm + self.skip_headers = skip_headers + self.is_windows = platform.system() == 'Windows' + self.header_filter = HeaderFilter(debug=debug, trace=trace, quiet=quiet, skip_limit=skip_headers_limit) + if hpsm: + self.crc8_maxim_dow_table = [] + self.crc8_generate_table() @staticmethod def __normalize(byte): @@ -127,7 +215,7 @@ def __normalize(byte): Parameters ---------- byte : int - The byte to normalize + The byte to normalise """ if byte < ASCII_0: return 0 @@ -137,11 +225,11 @@ def __normalize(byte): return byte if byte >= ASCII_a: return byte - if (byte >= 65) and (byte <= 90): + if (byte >= ASCII_A) and (byte <= ASCII_Z): return byte + 32 return 0 - def __skip_snippets(self, file: str, src: str) -> bool: + def __skip_snippets(self, file: str, src: str) -> bool: # noqa: PLR0911 """ Determine files that are not of interest based on their content or file extension Parameters @@ -160,20 +248,25 @@ def __skip_snippets(self, file: str, src: str) -> bool: for ending in SKIP_SNIPPET_EXT: if lower_file.endswith(ending): self.print_trace(f'Skipping snippets due to file ending: {file} - {ending}') - return True; + return True src_len = len(src) - if src_len == 0 or src_len <= MIN_FILE_SIZE: # Ignore empty or files that are too small + if src_len == 0 or src_len <= MIN_FILE_SIZE: # Ignore empty or files that are too small self.print_trace(f'Skipping snippets as the file is too small: {file} - {src_len}') return True - prefix = src[0:(MIN_FILE_SIZE-1)].lower().strip() - if len(prefix) > 0 and (prefix[0] == "{" or prefix[0] == "["): # Ignore json + prefix = src[0 : (MIN_FILE_SIZE - 1)].lower().strip() + if len(prefix) > 0 and (prefix[0] == '{' or prefix[0] == '['): # Ignore json self.print_trace(f'Skipping snippets as the file appears to be JSON: {file}') return True - if prefix.startswith(" MAX_LONG_LINE_CHARS: # Ignore long lines + return True # Ignore xml & html & ac3d + index = src.index('\n') if '\n' in src else (src_len - 1) # TODO still necessary if we have a binary check? + if len(src[0:index]) > MAX_LONG_LINE_CHARS: # Ignore long lines self.print_trace(f'Skipping snippets due to file line being too long: {file} - {MAX_LONG_LINE_CHARS}') return True return False @@ -201,40 +294,214 @@ def is_binary(self, path: str): :return: True if binary, False otherwise """ if path: - binary_path = is_binary(path) + try: + binary_path = is_binary(path) + except RuntimeError as e: + self.print_stderr(f'Warning: Failed to detect binary status for {path}. Assuming binary. Details:{e}') + return True if binary_path: self.print_trace(f'Detected binary file: {path}') return binary_path return False - def wfp_for_contents(self, file: str, bin_file: bool, contents: bytes) -> str: + def __strip_hpsm(self, file: str, hpsm: str) -> str: + """ + Strip off request HPSM IDs if requested + + :param file: name of the fingerprinted file + :param hpsm: HPSM string + :return: modified HPSM (if necessary) + """ + hpsm_len = len(hpsm) + if self.strip_hpsm_ids and hpsm_len > 1: + # Check for HPSM ID strings to remove. The size of the sequence must be conserved. + for hpsm_id in self.strip_hpsm_ids: + hpsm_id_index = hpsm.find(hpsm_id) + hpsm_id_len = len(hpsm_id) + # If the position is odd, we need to overwrite one byte before. + if hpsm_id_index % 2 == 1: + hpsm_id_index = hpsm_id_index - 1 + # If the size of the sequence is even, we need to overwrite one byte after. + if hpsm_id_len % 2 == 0: + hpsm_id_len = hpsm_id_len + 1 + hpsm_id_len = hpsm_id_len + 1 + # If the position is even and the size is odd, we need to overwrite one byte after. + elif hpsm_id_len % 2 == 1: + hpsm_id_len = hpsm_id_len + 1 + + to_remove = hpsm[hpsm_id_index : hpsm_id_index + hpsm_id_len] + self.print_debug(f'HPSM ID {to_remove} to replace') + # Calculate the XOR of each byte to produce the correct ignore sequence. + replacement = ''.join( + [format(int(to_remove[i : i + 2], 16) ^ 0xFF, '02x') for i in range(0, len(to_remove), 2)] + ) + + self.print_debug(f'HPSM ID replacement {replacement}') + # Overwrite HPSM bytes to be removed. + hpsm = hpsm.replace(to_remove, replacement) + if hpsm_len != len(hpsm): + self.print_stderr(f'wrong HPSM values from {file}') + return hpsm + + def __strip_snippets(self, file: str, wfp: str) -> str: + """ + Strip snippet IDs from the WFP + + :param file: name of fingerprinted file + :param wfp: WFP to clean + :return: updated WFP + """ + wfp_len = len(wfp) + for snippet_id in self.strip_snippet_ids: # Remove exact snippet strings + wfp = wfp.replace(snippet_id, '') + if wfp_len > len(wfp): + wfp = re.sub(r'(,)\1+', ',', wfp) # Remove multiple 'empty comma' blocks + wfp = wfp.replace(',\n', '\n') # Remove trailing comma + wfp = wfp.replace('=,', '=') # Remove leading comma + wfp = re.sub(r'\d+=\s+', '', wfp) # Cleanup empty lines + self.print_debug(f'Stripped snippet ids from {file}') + return wfp + + def __strip_lines_until_offset(self, file: str, wfp: str, line_offset: int) -> str: """ - Generate a Winnowing Finger Print (WFP) for the given file contents + Strip lines from the WFP up to and including the line_offset + + :param file: name of fingerprinted file + :param wfp: WFP to clean + :param line_offset: line number offset to strip up to + :return: updated WFP + """ + # No offset specified, return original WFP + if line_offset <= 0: + return wfp + lines = wfp.split('\n') + filtered_lines = [] + start_line_added = False + for line in lines: + # Check if a line contains snippet data (format: line_number=hash,hash,...) + line_details = line.split('=') + if line_details[0].isdigit(): + try: + line_num = int(line_details[0]) + # Keep lines that are after the offset + # (line_offset is the last line previous to real code) + if line_num > line_offset: + # Add the start_line tag before the first snippet line + if not start_line_added: + filtered_lines.append(f'start_line={line_offset}') + start_line_added = True + filtered_lines.append(line) + except (ValueError, IndexError) as e: + self.print_stderr(f'Error decoding line number from line {line} in {file}: {e}') + # Keep non-snippet lines (like file=, hpsm=, etc.) + filtered_lines.append(line) + else: + # Keep non-snippet lines (like file=, hpsm=, etc.) + filtered_lines.append(line) + # End for loop comment + wfp = '\n'.join(filtered_lines) + if start_line_added: + self.print_debug(f'Stripped lines up to offset {line_offset} from {file}') + return wfp + + def __detect_line_endings(self, contents: bytes) -> Tuple[bool, bool, bool]: + """Detect the types of line endings present in file contents. + + Args: + contents: File contents as bytes. + + Returns: + Tuple of (has_crlf, has_lf_only, has_cr_only, has_mixed) indicating which line ending types are present. + """ + if not contents: + self.print_debug('Warning: No file contents provided') + has_crlf = b'\r\n' in contents + # For LF detection, we need to find LF that's not part of CRLF + content_without_crlf = contents.replace(b'\r\n', b'') + has_standalone_lf = b'\n' in content_without_crlf + # For CR detection, we need to find CR that's not part of CRLF + has_standalone_cr = b'\r' in content_without_crlf + return has_crlf, has_standalone_lf, has_standalone_cr + + def __calculate_opposite_line_ending_hash(self, contents: bytes): + """Calculate hash for contents with opposite line endings. + + If the file is primarily Unix (LF), calculates Windows (CRLF) hash. + If the file is primarily Windows (CRLF), calculates Unix (LF) hash. + + Args: + contents: File contents as bytes. + + Returns: + Hash with opposite line endings as hex string, or None if no line endings detected. + """ + has_crlf, has_standalone_lf, has_standalone_cr = self.__detect_line_endings(contents) + if not has_crlf and not has_standalone_lf and not has_standalone_cr: + self.print_debug('No line endings detected in file contents') + return None + # Normalise all line endings to LF first + normalized = contents.replace(b'\r\n', b'\n').replace(b'\r', b'\n') + # Determine the dominant line ending type + if has_crlf and not has_standalone_lf and not has_standalone_cr: + # File is Windows (CRLF) - produce Unix (LF) hash + opposite_contents = normalized + else: + # File is Unix (LF/CR) or mixed - produce Windows (CRLF) hash + opposite_contents = normalized.replace(b'\n', b'\r\n') + # Return the MD5 hash of the opposite contents + return hashlib.md5(opposite_contents).hexdigest() + + def wfp_for_contents(self, file: str, bin_file: bool, contents: bytes) -> str: # noqa: PLR0912, PLR0915 + """ + Generate a Winnowing fingerprint (WFP) for the given file contents Parameters ---------- - file: str - file to fingerprint - contents: bytes - file contents + :param file: file to fingerprint + :param bin_file: binary file or not + :param contents: file contents Return ------ WFP string """ file_md5 = hashlib.md5(contents).hexdigest() + if self.skip_md5_ids and file_md5 in self.skip_md5_ids: + self.print_debug(f'Skipping MD5 file name for {file_md5}: {file}') + return '' # Print file line content_length = len(contents) - wfp = 'file={0},{1},{2}\n'.format(file_md5, content_length, file) + original_filename = file + if self.is_windows: + original_filename = file.replace('\\', '/') + wfp_filename = repr(original_filename).strip("'") # return a utf-8 compatible version of the filename + # hide the real size of the file and its name but keep the suffix + if self.obfuscate: + wfp_filename = f'{self.ob_count}{pathlib.Path(original_filename).suffix}' + self.ob_count = self.ob_count + 1 + self.file_map[wfp_filename] = original_filename # Save the file name map for later (reverse lookup) + # Construct the WFP header + wfp = 'file={0},{1},{2}\n'.format(file_md5, content_length, wfp_filename) + # Add the opposite line ending hash based on line ending analysis + if not bin_file: + opposite_hash = self.__calculate_opposite_line_ending_hash(contents) + if opposite_hash is not None: + wfp += f'fh2={opposite_hash}\n' # We don't process snippets for binaries, or other uninteresting files, or if we're requested to skip - if bin_file or self.skip_snippets or self.__skip_snippets(file, contents.decode('utf-8', 'ignore')): + decoded_contents = contents.decode('utf-8', 'ignore') + if bin_file or self.skip_snippets or self.__skip_snippets(file, decoded_contents): return wfp + # Add HPSM (calculated from original contents, not filtered) + if self.hpsm: + hpsm = self.__strip_hpsm(file, self.calc_hpsm(contents)) + if len(hpsm) > 0: + wfp += f'hpsm={hpsm}\n' # Initialize variables - gram = "" + gram = '' window = [] - line = 1 + line = 1 # Line counter for WFP generation last_hash = MAX_CRC32 last_line = 0 - output = "" - # Otherwise recurse src_content and calculate Winnowing hashes + output = '' + # Otherwise, recurse src_content and calculate Winnowing hashes for byte in contents: if byte == ASCII_LF: line += 1 @@ -260,14 +527,17 @@ def wfp_for_contents(self, file: str, bin_file: bool, contents: bytes) -> str: crc = crc32c(min_hash.to_bytes(4, byteorder='little')) crc_hex = '{:08x}'.format(crc) if last_line != line: - if output: - if self.size_limit and \ - (len(wfp.encode("utf-8")) + len(output.encode("utf-8"))) > self.max_post_size: - self.print_debug(f'Truncating WFP (64k limit) for: {file}') + if output != '': + if ( + self.size_limit + and (len(wfp.encode('utf-8')) + len(output.encode('utf-8'))) + > self.max_post_size + ): + self.print_debug(f'Truncating WFP ({self.max_post_size} limit) for: {file}') output = '' - break # Stop collecting snippets as it's over 64k + break # Stop collecting snippets as it's over 64k wfp += output + '\n' - output = "%d=%s" % (line, crc_hex) + output = '%d=%s' % (line, crc_hex) else: output += ',' + crc_hex @@ -277,11 +547,106 @@ def wfp_for_contents(self, file: str, bin_file: bool, contents: bytes) -> str: window.pop(0) # Shift gram gram = gram[1:] - if output and (not self.size_limit or (len(wfp.encode("utf-8")) + len(output.encode("utf-8"))) < self.max_post_size): - wfp += output + '\n' - + if output != '': + if not self.size_limit or (len(wfp.encode('utf-8')) + len(output.encode('utf-8'))) < self.max_post_size: + wfp += output + '\n' + else: + self.print_debug(f'Warning: skipping output in WFP for {file} - "{output}"') + # Warn if we don't have any WFP content + if wfp is None or wfp == '': + self.print_stderr(f'Warning: No WFP content data for {file}') + else: + # Apply line filter to remove headers, comments, and imports from the beginning (if enabled) + if self.skip_headers: + line_offset = self.header_filter.filter(file, decoded_contents) + if line_offset > 0: + wfp = self.__strip_lines_until_offset(file, wfp, line_offset) + # Strip snippet IDs from the WFP (if enabled) + if self.strip_snippet_ids: + wfp = self.__strip_snippets(file, wfp) + # Return the WFP contents return wfp + def calc_hpsm(self, content): + """ + Calculate the HPSM data for this content + + :param content: content bytes to calculate + :return: HPSM encoded data + """ + list_normalized = [] # Array of numbers + crc_lines = [] # Array of numbers that represent the crc8_maxim for each line of the file + last_line = 0 + for i, byte in enumerate(content): + c = byte + if c == ASCII_LF: # When there is a new line + if list_normalized: + crc_lines.append(self.crc8_buffer(list_normalized)) + list_normalized = [] + elif last_line + 1 == i: + crc_lines.append(0xFF) + elif i - last_line > 1: + crc_lines.append(0x00) + last_line = i + else: + c_normalized = self.__normalize(c) + if c_normalized != 0: + list_normalized.append(c_normalized) + hpsm = ''.join('{:02x}'.format(x) for x in crc_lines) + return hpsm + + def crc8_generate_table(self): + """ + Generate the CRC8 maxim dow table + + :return: nothing + """ + if not self.crc8_maxim_dow_table or len(self.crc8_maxim_dow_table) == 0: + for i in range(CRC8_MAXIM_DOW_TABLE_SIZE): + self.crc8_maxim_dow_table.append(self.crc8_byte_checksum(0, i)) + + @staticmethod + def crc8_byte_checksum(crc: int, byte): + """ + Calculate the CRC8 checksum for the given byte + + :param crc: + :param byte: + :return: CRC for the byte + """ + crc ^= byte + for count in range(8): + is_set = crc & 0x01 + crc >>= 1 + if is_set: + crc ^= CRC8_MAXIM_DOW_POLYNOMIAL + return crc + + def crc8_byte(self, crc: int, byte): + """ + Calculate the CRC8 for the given CRC & Byte + + :param crc: + :param byte: + :return: + """ + index = byte ^ crc + return self.crc8_maxim_dow_table[index] ^ (crc >> 8) + + def crc8_buffer(self, buffer): + """ + Calculate the CRC for the given buffer list + + :param buffer: + :return: + """ + crc = CRC8_MAXIM_DOW_INITIAL + for index in range(len(buffer)): + crc = self.crc8_byte(crc, buffer[index]) + crc ^= CRC8_MAXIM_DOW_FINAL # Bitwise OR (XOR) of crc in Maxim Dow Final + return crc + + # # End of Winnowing Class -# \ No newline at end of file +# diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/cli-test.py b/tests/cli-test.py new file mode 100644 index 00000000..1b0592fe --- /dev/null +++ b/tests/cli-test.py @@ -0,0 +1,66 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +import unittest + +from scanoss.cli import process_req_headers + + +class MyTestCase(unittest.TestCase): + + def test_header_argument_processor(self): + # Test input + headers_input = [ + 'x-api-key:12334', + 'generic-header-1:generic-header-value-1', + ' space-header : value-space-header', + 'generic-header2 generic-header-value-2' # Note: missing colon separator + ] + + # Process headers + processed_headers = process_req_headers(headers_input) + + # Expected results (as a dictionary for easier comparison) + expected_headers = { + 'x-api-key': '12334', + 'generic-header-1': 'generic-header-value-1', + 'space-header': 'value-space-header' + # Note: generic-header2 not included as it doesn't have a colon separator + } + + # Test exact dictionary equality + self.assertEqual(processed_headers, expected_headers, + f"Headers don't match expected values.\nGot:" + f" {processed_headers}\nExpected: {expected_headers}") + + # Additional tests for specific cases + self.assertIn('x-api-key', processed_headers, "Required header 'x-api-key' missing") + self.assertEqual( + processed_headers['x-api-key'], + '12334', + "Header value for 'x-api-key' is incorrect") + + # Test that the malformed header was not included + self.assertNotIn('generic-header2', processed_headers, + "Malformed header without colon separator should not be included") \ No newline at end of file diff --git a/tests/data/.gitignore b/tests/data/.gitignore new file mode 100644 index 00000000..fd14195c --- /dev/null +++ b/tests/data/.gitignore @@ -0,0 +1,2 @@ +src +!test_src_files.tar.gz \ No newline at end of file diff --git a/tests/data/comp-search-input.json b/tests/data/comp-search-input.json new file mode 100644 index 00000000..fa02b463 --- /dev/null +++ b/tests/data/comp-search-input.json @@ -0,0 +1,5 @@ +{ + "search": "unoconv", + "package": "github", + "limit": 5 +} \ No newline at end of file diff --git a/tests/data/comp-versions-input.json b/tests/data/comp-versions-input.json new file mode 100644 index 00000000..9b7c95aa --- /dev/null +++ b/tests/data/comp-versions-input.json @@ -0,0 +1,4 @@ +{ + "purl": "pkg:github/unoconv/unoconv", + "limit": 10 +} \ No newline at end of file diff --git a/tests/data/empty-result.json b/tests/data/empty-result.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/tests/data/empty-result.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/data/package.json b/tests/data/package.json new file mode 100644 index 00000000..6fb162a1 --- /dev/null +++ b/tests/data/package.json @@ -0,0 +1,25 @@ +{ + "name": "vue", + "version": "2.6.12", + "description": "Reactive, component-oriented view layer for modern web interfaces.", + "main": "dist/vue.runtime.common.js", + "module": "dist/vue.runtime.esm.js", + "unpkg": "dist/vue.js", + "jsdelivr": "dist/vue.js", + "typings": "types/index.d.ts", + "files": [ + "src", + "dist/*.js", + "types/*.d.ts" + ], + "author": "Evan You", + "license": "MIT", + "homepage": "https://github.com/vuejs/vue#readme", + "dependencies": { + "uuid": "^9.0.0", + "xml-js": "^1.6.11" + }, + "devDependencies": { + "@babel/core": ">0.2.0" + } +} diff --git a/tests/data/purl-input-with-requirement.json b/tests/data/purl-input-with-requirement.json new file mode 100644 index 00000000..c7cfd716 --- /dev/null +++ b/tests/data/purl-input-with-requirement.json @@ -0,0 +1,8 @@ +{ + "purls": [ + { + "purl": "pkg:github/torvalds/linux", + "requirement": ">v5.13" + } + ] +} \ No newline at end of file diff --git a/tests/data/purl-input.json b/tests/data/purl-input.json new file mode 100644 index 00000000..93e8e2f3 --- /dev/null +++ b/tests/data/purl-input.json @@ -0,0 +1,7 @@ +{ + "purls": [ + { + "purl": "pkg:github/torvalds/linux@v5.13" + } + ] +} \ No newline at end of file diff --git a/tests/data/requirements.txt b/tests/data/requirements.txt index bee304b6..93683ed9 100644 --- a/tests/data/requirements.txt +++ b/tests/data/requirements.txt @@ -2,3 +2,5 @@ requests crc32c>=2.2 binaryornot progress +grpcio>=1.73.1 +protobuf>=6.3.1 diff --git a/tests/data/result-no-copyleft.json b/tests/data/result-no-copyleft.json new file mode 100644 index 00000000..8695018b --- /dev/null +++ b/tests/data/result-no-copyleft.json @@ -0,0 +1,238 @@ +{ + "inc/crc32c.h": [ + { + "id": "none", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + } + } + ], + "inc/json.h": [ + { + "component": "scanner.c", + "file": "scanner.c-1.3.3/external/inc/json.h", + "file_hash": "e91a03b850651dd56dd979ba92668a19", + "file_url": "https://api.osskb.org/file_contents/e91a03b850651dd56dd979ba92668a19", + "id": "file", + "latest": "1.3.4", + "licenses": [], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/scanner.c" + ], + "release_date": "2021-05-26", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "e91a03b850651dd56dd979ba92668a19", + "status": "pending", + "url": "https://github.com/scanoss/scanner.c", + "url_hash": "2d1700ba496453d779d4987255feb5f2", + "url_stats": {}, + "vendor": "scanoss", + "version": "1.3.3" + } + ], + "inc/log.h": [ + { + "component": "scanner.c", + "file": "scanner.c-1.1.4/external/inc/log.h", + "file_hash": "36ae4f65e9302539357a64ddb8e35c28", + "file_url": "https://api.osskb.org/file_contents/36ae4f65e9302539357a64ddb8e35c28", + "id": "file", + "latest": "1.3.4", + "licenses": [], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/scanner.c" + ], + "release_date": "2021-01-11", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "36ae4f65e9302539357a64ddb8e35c28", + "status": "pending", + "url": "https://github.com/scanoss/scanner.c", + "url_hash": "1dcd883dc73d16f3ce2cf4f1f9854c7b", + "url_stats": {}, + "vendor": "scanoss", + "version": "1.1.4" + } + ], + "inc/winnowing.h": [ + { + "component": "engine", + "file": "external/inc/winnowing.h", + "file_hash": "d58b03e8ef411204db0fb991f778444a", + "file_url": "https://api.osskb.org/file_contents/d58b03e8ef411204db0fb991f778444a", + "id": "file", + "latest": "5.4.8", + "licenses": [], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/engine" + ], + "release_date": "2024-02-27", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "d58b03e8ef411204db0fb991f778444a", + "status": "identified", + "url": "https://github.com/scanoss/engine", + "url_hash": "5107cd431b8b6c7836c84e997bab01ec", + "url_stats": {}, + "vendor": "scanoss", + "version": "5.4.0" + } + ], + "src/crc32c.c": [ + { + "component": "wfp", + "file": "wfp-6afc1f6163d1d6c8d03ff5211a0571118e08da1f/src/external/crc32c/crc32c.c", + "file_hash": "0fe279946d388ef07d9c3f6e3ffb8ebe", + "file_url": "https://api.osskb.org/file_contents/0fe279946d388ef07d9c3f6e3ffb8ebe", + "id": "file", + "latest": "0ed473d", + "licenses": [], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/wfp" + ], + "release_date": "2020-07-12", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "0fe279946d388ef07d9c3f6e3ffb8ebe", + "status": "pending", + "url": "https://github.com/scanoss/wfp", + "url_hash": "9b36f30d422d7f77854f298f63c55256", + "url_stats": {}, + "vendor": "scanoss", + "version": "6afc1f6" + } + ], + "src/json.c": [ + { + "component": "scanner.c", + "file": "scanner.c-1.3.3/external/src/json.c", + "file_hash": "8e4d433c1547b59681379e9fe9960546", + "file_url": "https://api.osskb.org/file_contents/8e4d433c1547b59681379e9fe9960546", + "id": "file", + "latest": "1.3.4", + "licenses": [], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/scanner.c" + ], + "release_date": "2021-05-26", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "8e4d433c1547b59681379e9fe9960546", + "status": "pending", + "url": "https://github.com/scanoss/scanner.c", + "url_hash": "2d1700ba496453d779d4987255feb5f2", + "url_stats": {}, + "vendor": "scanoss", + "version": "1.3.3" + } + ], + "src/log.c": [ + { + "component": "scanner.c", + "file": "scanner.c-1.3.3/external/src/log.c", + "file_hash": "f00c8a010806ff1593b15c7cbff7e594", + "file_url": "https://api.osskb.org/file_contents/f00c8a010806ff1593b15c7cbff7e594", + "id": "file", + "latest": "1.3.4", + "licenses": [], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/scanner.c" + ], + "release_date": "2021-05-26", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "f00c8a010806ff1593b15c7cbff7e594", + "status": "pending", + "url": "https://github.com/scanoss/scanner.c", + "url_hash": "2d1700ba496453d779d4987255feb5f2", + "url_stats": {}, + "vendor": "scanoss", + "version": "1.3.3" + } + ], + "src/winnowing.c": [ + { + "component": "engine", + "file": "external/src/winnowing.c", + "file_hash": "b298d620599a6ad04e2613dfdc9d3f7b", + "file_url": "https://api.osskb.org/file_contents/b298d620599a6ad04e2613dfdc9d3f7b", + "id": "snippet", + "latest": "4.3.4", + "licenses": [], + "lines": "32-161", + "matched": "80%", + "oss_lines": "29-158", + "purl": [ + "pkg:github/scanoss/engine" + ], + "release_date": "2020-12-30", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "af368f0dfee072472cf57f0694975b28", + "status": "pending", + "url": "https://github.com/scanoss/engine", + "url_hash": "0c0c7aaf65ab9e2e10562d188ff3e511", + "url_stats": {}, + "vendor": "scanoss", + "version": "4.0.4" + } + ] +} diff --git a/tests/data/result.json b/tests/data/result.json new file mode 100644 index 00000000..3df4c7dc --- /dev/null +++ b/tests/data/result.json @@ -0,0 +1,549 @@ +{ + "inc/crc32c.h": [ + { + "id": "none", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + } + } + ], + "inc/log.c": [ + { + "component": "scanner.c", + "file": "scanner.c-1.3.3/external/inc/json.h", + "file_hash": "e91a03b850651dd56dd979ba92668a19", + "file_url": "https://api.osskb.org/file_contents/e91a03b850651dd56dd979ba92668a19", + "id": "file", + "latest": "1.3.4", + "licenses": [], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/jenkins-pipeline-example" + ], + "release_date": "2021-05-26", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "e91a03b850651dd56dd979ba92668a19", + "status": "pending", + "url": "https://github.com/scanoss/scanner.c", + "url_hash": "2d1700ba496453d779d4987255feb5f2", + "url_stats": {}, + "vendor": "scanoss", + "version": "1.3.3" + } + ], + "inc/json.h": [ + { + "component": "scanner.c", + "file": "scanner.c-1.3.3/external/inc/json.h", + "file_hash": "e91a03b850651dd56dd979ba92668a19", + "file_url": "https://api.osskb.org/file_contents/e91a03b850651dd56dd979ba92668a19", + "id": "file", + "latest": "1.3.4", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/BSD-2-Clause.txt", + "copyleft": "no", + "name": "BSD-2-Clause", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "no", + "source": "file_header", + "url": "https://spdx.org/licenses/BSD-2-Clause.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/BSD-2-Clause.txt", + "copyleft": "no", + "name": "BSD-2-Clause", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "no", + "source": "scancode", + "url": "https://spdx.org/licenses/BSD-2-Clause.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, ECL-2.0, FTL, IJG, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-only", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "yes", + "source": "component_declared", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/scanner.c" + ], + "release_date": "2021-05-26", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "e91a03b850651dd56dd979ba92668a19", + "status": "pending", + "url": "https://github.com/scanoss/scanner.c", + "url_hash": "2d1700ba496453d779d4987255feb5f2", + "url_stats": {}, + "vendor": "scanoss", + "version": "1.3.3" + } + ], + "inc/component.c": [ + { + "component": "scanner.c", + "file": "scanner.c-1.3.3/external/inc/json.h", + "file_hash": "e91a03b850651dd56dd979ba92668a19", + "file_url": "https://api.osskb.org/file_contents/e91a03b850651dd56dd979ba92668a19", + "id": "file", + "latest": "1.3.4", + "licenses": [], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/scanner.c" + ], + "release_date": "2021-05-26", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "e91a03b850651dd56dd979ba92668a19", + "status": "pending", + "url": "https://github.com/scanoss/scanner.c", + "url_hash": "2d1700ba496453d779d4987255feb5f2", + "url_stats": {}, + "vendor": "scanoss", + "version": "1.3.3" + } + ], + "inc/helper.c": [ + { + "component": "scanner.c", + "file": "scanner.c-1.3.3/external/inc/json.h", + "file_hash": "e91a03b850651dd56dd979ba92668a19", + "file_url": "https://api.osskb.org/file_contents/e91a03b850651dd56dd979ba92668a19", + "id": "file", + "latest": "1.3.4", + "licenses": [], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/scanner.c" + ], + "release_date": "2021-05-26", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "e91a03b850651dd56dd979ba92668a19", + "status": "pending", + "url": "https://github.com/scanoss/scanner.c", + "url_hash": "2d1700ba496453d779d4987255feb5f2", + "url_stats": {}, + "vendor": "scanoss", + "version": "1.3.3" + } + ], + "inc/log.h": [ + { + "component": "scanner.c", + "file": "scanner.c-1.1.4/external/inc/log.h", + "file_hash": "36ae4f65e9302539357a64ddb8e35c28", + "file_url": "https://api.osskb.org/file_contents/36ae4f65e9302539357a64ddb8e35c28", + "id": "file", + "latest": "1.3.4", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, ECL-2.0, FTL, IJG, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-only", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "yes", + "source": "component_declared", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/scanner.c" + ], + "release_date": "2021-01-11", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "36ae4f65e9302539357a64ddb8e35c28", + "status": "pending", + "url": "https://github.com/scanoss/scanner.c", + "url_hash": "1dcd883dc73d16f3ce2cf4f1f9854c7b", + "url_stats": {}, + "vendor": "scanoss", + "version": "1.1.4" + } + ], + "inc/winnowing.h": [ + { + "component": "engine", + "file": "external/inc/winnowing.h", + "file_hash": "d58b03e8ef411204db0fb991f778444a", + "file_url": "https://api.osskb.org/file_contents/d58b03e8ef411204db0fb991f778444a", + "id": "file", + "latest": "5.4.8", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, ECL-2.0, FTL, IJG, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-only", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "yes", + "source": "component_declared", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, ECL-2.0, FTL, IJG, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-only", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "yes", + "source": "license_file", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/engine" + ], + "release_date": "2024-02-27", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "d58b03e8ef411204db0fb991f778444a", + "status": "identified", + "url": "https://github.com/scanoss/engine", + "url_hash": "5107cd431b8b6c7836c84e997bab01ec", + "url_stats": {}, + "vendor": "scanoss", + "version": "5.4.0" + } + ], + "src/crc32c.c": [ + { + "component": "wfp", + "file": "wfp-6afc1f6163d1d6c8d03ff5211a0571118e08da1f/src/external/crc32c/crc32c.c", + "file_hash": "0fe279946d388ef07d9c3f6e3ffb8ebe", + "file_url": "https://api.osskb.org/file_contents/0fe279946d388ef07d9c3f6e3ffb8ebe", + "id": "file", + "latest": "0ed473d", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/Zlib.txt", + "copyleft": "no", + "name": "Zlib", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "no", + "source": "scancode", + "url": "https://spdx.org/licenses/Zlib.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/Zlib.txt", + "copyleft": "no", + "name": "Zlib", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "no", + "source": "file_header", + "url": "https://spdx.org/licenses/Zlib.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, ECL-2.0, FTL, IJG, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-only", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "yes", + "source": "license_file", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, ECL-2.0, FTL, IJG, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-only", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "yes", + "source": "component_declared", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/wfp" + ], + "release_date": "2020-07-12", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "0fe279946d388ef07d9c3f6e3ffb8ebe", + "status": "pending", + "url": "https://github.com/scanoss/wfp", + "url_hash": "9b36f30d422d7f77854f298f63c55256", + "url_stats": {}, + "vendor": "scanoss", + "version": "6afc1f6" + } + ], + "src/json.c": [ + { + "component": "scanner.c", + "file": "scanner.c-1.3.3/external/src/json.c", + "file_hash": "8e4d433c1547b59681379e9fe9960546", + "file_url": "https://api.osskb.org/file_contents/8e4d433c1547b59681379e9fe9960546", + "id": "file", + "latest": "1.3.4", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/BSD-2-Clause.txt", + "copyleft": "no", + "name": "BSD-2-Clause", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "no", + "source": "file_header", + "url": "https://spdx.org/licenses/BSD-2-Clause.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, ECL-2.0, FTL, IJG, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-only", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "yes", + "source": "component_declared", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/scanner.c" + ], + "release_date": "2021-05-26", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "8e4d433c1547b59681379e9fe9960546", + "status": "pending", + "url": "https://github.com/scanoss/scanner.c", + "url_hash": "2d1700ba496453d779d4987255feb5f2", + "url_stats": {}, + "vendor": "scanoss", + "version": "1.3.3" + } + ], + "src/log.c": [ + { + "component": "scanner.c", + "file": "scanner.c-1.3.3/external/src/log.c", + "file_hash": "f00c8a010806ff1593b15c7cbff7e594", + "file_url": "https://api.osskb.org/file_contents/f00c8a010806ff1593b15c7cbff7e594", + "id": "file", + "latest": "1.3.4", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, ECL-2.0, FTL, IJG, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-only", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "yes", + "source": "component_declared", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/scanner.c" + ], + "release_date": "2021-05-26", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "f00c8a010806ff1593b15c7cbff7e594", + "status": "pending", + "url": "https://github.com/scanoss/scanner.c", + "url_hash": "2d1700ba496453d779d4987255feb5f2", + "url_stats": {}, + "vendor": "scanoss", + "version": "1.3.3" + } + ], + "src/winnowing.c": [ + { + "component": "engine", + "file": "external/src/winnowing.c", + "file_hash": "b298d620599a6ad04e2613dfdc9d3f7b", + "file_url": "https://api.osskb.org/file_contents/b298d620599a6ad04e2613dfdc9d3f7b", + "id": "snippet", + "latest": "4.3.4", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-or-later.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, ECL-2.0, FTL, IJG, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-or-later", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "yes", + "source": "file_spdx_tag", + "url": "https://spdx.org/licenses/GPL-2.0-or-later.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-1.0-or-later.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, ECL-2.0, FTL, IJG, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-1.0-or-later", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "no", + "source": "scancode", + "url": "https://spdx.org/licenses/GPL-1.0-or-later.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-or-later.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, ECL-2.0, FTL, IJG, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-or-later", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "yes", + "source": "scancode", + "url": "https://spdx.org/licenses/GPL-2.0-or-later.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, ECL-2.0, FTL, IJG, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-only", + "osadl_updated": "2024-08-11T02:21:00+00:00", + "patent_hints": "yes", + "source": "component_declared", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + }, + { + "name": "MIT", + "patent_hints": "no", + "copyleft": "no", + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/MIT.txt", + "osadl_updated": "2024-09-20T09:32:00+0000", + "source": "scancode", + "url": "https://spdx.org/licenses/MIT.html" + } + ], + "lines": "32-161", + "matched": "80%", + "oss_lines": "29-158", + "purl": [ + "pkg:github/scanoss/engine" + ], + "release_date": "2020-12-30", + "server": { + "kb_version": { + "daily": "24.08.16", + "monthly": "24.07" + }, + "version": "5.4.8" + }, + "source_hash": "af368f0dfee072472cf57f0694975b28", + "status": "identified", + "url": "https://github.com/scanoss/engine", + "url_hash": "0c0c7aaf65ab9e2e10562d188ff3e511", + "url_stats": {}, + "vendor": "scanoss", + "version": "4.0.4" + } + ], + "example_codebase/dependencies/package.json": [ + { + "dependencies": [ + { + "component": "@electron/rebuild", + "licenses": [ + { + "is_spdx_approved": true, + "name": "MIT", + "spdx_id": "MIT" + } + ], + "purl": "pkg:npm/%40electron/rebuild", + "url": "https://www.npmjs.com/package/%40electron/rebuild", + "version": "3.7.0" + }, + { + "component": "@emotion/react", + "licenses": [ + { + "is_spdx_approved": true, + "name": "MIT", + "spdx_id": "MIT" + } + ], + "purl": "pkg:npm/%40emotion/react", + "url": "https://www.npmjs.com/package/%40emotion/react", + "version": "11.13.3" + } + ], + "id": "dependency", + "status": "pending" + } + ] +} diff --git a/tests/data/scanoss.json b/tests/data/scanoss.json new file mode 100644 index 00000000..fac0b807 --- /dev/null +++ b/tests/data/scanoss.json @@ -0,0 +1,53 @@ +{ + "bom": { + "include": [], + "remove": [ + { + "path": "scanoss_settings.py", + "purl": "pkg:github/scanoss/scanoss.py" + }, + { + "path": "test_file_path.go", + "purl": "pkg:github/scanoss/scanoss.py" + }, + { + "purl": "matching/purl" + } + ], + "replace": [ + { + "path": "full_match_test.py", + "purl": "pkg:github/scanoss/full_match_test.py", + "replace_with": "pkg:github/scanoss/full_match_replaced.py" + }, + { + "purl": "pkg:github/scanoss/only_purl_match.py", + "replace_with": "pkg:github/scanoss/only_purl_match_replaced.py" + } + ] + }, + "settings": { + "proxy": { + "host": "http://root-proxy:8080" + }, + "http_config": { + "base_uri": "https://root-api.scanoss.com", + "ignore_cert_errors": false + }, + "file_snippet": { + "proxy": { + "host": "http://file-snippet-proxy:8080" + }, + "http_config": { + "base_uri": "https://file-snippet-api.scanoss.com", + "ignore_cert_errors": true + }, + "min_snippet_hits": 10, + "min_snippet_lines": 5, + "honour_file_exts": true, + "ranking_enabled": true, + "ranking_threshold": 10 + } + } +} + diff --git a/tests/data/test_src_files.tar.gz b/tests/data/test_src_files.tar.gz new file mode 100644 index 00000000..682c9262 Binary files /dev/null and b/tests/data/test_src_files.tar.gz differ diff --git a/tests/grpc-client-test.py b/tests/grpc-client-test.py index 5b4a0419..680820f3 100644 --- a/tests/grpc-client-test.py +++ b/tests/grpc-client-test.py @@ -1,29 +1,34 @@ """ - SPDX-License-Identifier: MIT - - Copyright (c) 2021, SCANOSS - - 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. +SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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. """ + import os import unittest +from unittest.mock import patch +from io import StringIO + +from scanoss.components import Components from scanoss.scancodedeps import ScancodeDeps from scanoss.scanossgrpc import ScanossGrpc @@ -32,18 +37,21 @@ class MyTestCase(unittest.TestCase): """ Unit test cases for GRPC comms """ - TEST_LOCAL = os.getenv("SCANOSS_TEST_LOCAL", 'True').lower() in ('true', '1', 't', 'yes', 'y') + + TEST_LOCAL = os.getenv('SCANOSS_TEST_LOCAL', 'True').lower() in ('true', '1', 't', 'yes', 'y') def test_grpc_dep_echo(self): """ Test the basic echo rpc call on the local server """ if MyTestCase.TEST_LOCAL: + server_type = 'local' grpc_client = ScanossGrpc(debug=True, url='localhost:50051') else: + server_type = 'remote' grpc_client = ScanossGrpc(debug=True) - echo_resp = grpc_client.deps_echo('testing dep echo') - print(f'Echo Resp: {echo_resp}') + echo_resp = grpc_client.deps_echo(f'testing dep echo ({server_type})') + print(f'Echo Resp ({server_type}): {echo_resp}') self.assertIsNotNone(echo_resp) def test_grpc_get_dependencies(self): @@ -51,21 +59,79 @@ def test_grpc_get_dependencies(self): Test getting dependencies from the local gRPC server """ sc_deps = ScancodeDeps(debug=True) - dep_file = "data/scancode-deps.json" + dep_file = 'data/scancode-deps.json' deps = sc_deps.produce_from_file(dep_file) print(f'Dependency JSON: {deps}') self.assertIsNotNone(deps) if MyTestCase.TEST_LOCAL: + server_type = 'local' grpc_client = ScanossGrpc(debug=True, url='localhost:50051') else: + server_type = 'remote' grpc_client = ScanossGrpc(debug=True) resp = grpc_client.get_dependencies(deps) - print(f'Resp: {resp}') + print(f'Resp ({server_type}): {resp}') self.assertIsNotNone(resp) - dep_files = resp.get("files") + dep_files = resp.get('files') if dep_files and len(dep_files) > 0: for dep_file in dep_files: - file = dep_file.pop("file", None) + file = dep_file.pop('file', None) print(f'File: {file} - {dep_file}') + def test_load_purls_array(self): + comps = Components(debug=True, trace=True) + # Expected value as a dictionary, not a string + expected_value = { + 'purls': [ + {'purl': 'pkg:github/unoconv/unoconv'}, + {'purl': 'pkg:github/torvalds/linux@v5.13'} + ] + } + components = comps.load_purls(purls=["pkg:github/unoconv/unoconv", "pkg:github/torvalds/linux@v5.13"]) + self.assertEqual(components,expected_value) + + @patch('sys.stderr', new_callable=StringIO) + def test_load_purls_array_malformed(self, mock_stderr): + comps = Components(debug=True, trace=True) + components = comps.load_purls(purls=[1, "pkg:github/torvalds/linux@v5.13"]) + self.assertEqual(components,None) + self.assertIn('ERROR: PURLs must be a list of strings.', mock_stderr.getvalue()) + + @patch('sys.stderr', new_callable=StringIO) + def test_load_purls_file_malformed(self, mock_stderr): + comps = Components(debug=True, trace=True) + components = comps.load_purls(json_file='./data/malformed-purl-input.json') + # Ensure the method returned None (indicating a failure) + self.assertIsNone(components) + # Check if the correct error message was printed to stderr + self.assertIn('ERROR: No PURLs parsed from request.', mock_stderr.getvalue()) + + def test_load_purls_file(self): + comps = Components(debug=True, trace=True) + expected_value = { + 'purls': [ + { + 'purl': 'pkg:github/torvalds/linux@v5.13' + } + ] + } + components = comps.load_purls( json_file='./data/purl-input.json') + print(components) + # Ensure the method returned None (indicating a failure) + self.assertEqual(components,expected_value) + + def test_grpc_generic_metadata(self): + grpc_client = ScanossGrpc(debug=True, req_headers={'x-api-key': '123455', + 'generic-header': 'generic-header-value'}) + required_keys = ('x-api-key', 'user-agent', 'x-scanoss-client', 'generic-header') + valid_metadata = True + for key, value in grpc_client.metadata: + if key not in required_keys: + valid_metadata = False + self.assertTrue(valid_metadata) + + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/scancodedeps-test.py b/tests/scancodedeps-test.py index 1624aa0d..fd754a8b 100644 --- a/tests/scancodedeps-test.py +++ b/tests/scancodedeps-test.py @@ -1,46 +1,50 @@ """ - SPDX-License-Identifier: MIT - - Copyright (c) 2021, SCANOSS - - 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. +SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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. """ + +import json import os +import tempfile import unittest from scanoss.scancodedeps import ScancodeDeps from scanoss.scanossgrpc import ScanossGrpc -from scanoss.threadeddependencies import ThreadedDependencies +from scanoss.threadeddependencies import SCOPE, ThreadedDependencies class MyTestCase(unittest.TestCase): """ Unit test cases for Scancode Dependency analysis """ - TEST_LOCAL = os.getenv("SCANOSS_TEST_LOCAL", 'True').lower() in ('true', '1', 't', 'yes', 'y') + + TEST_LOCAL = os.getenv('SCANOSS_TEST_LOCAL', 'False').lower() in ('true', '1', 't', 'yes', 'y') def test_deps_parse(self): """ Parse the saved scancode dependency data file """ sc_deps = ScancodeDeps(debug=True) - dep_file = "data/scancode-deps.json" + dep_file = 'data/scancode-deps.json' deps = sc_deps.produce_from_file(dep_file) print(f'Dependency JSON: {deps}') self.assertIsNotNone(deps) @@ -51,7 +55,7 @@ def test_scan_dir(self): """ sc_deps = ScancodeDeps(debug=True) - self.assertTrue(sc_deps.run_scan(what_to_scan=".")) + self.assertTrue(sc_deps.run_scan(what_to_scan='.')) deps = sc_deps.produce_from_file() sc_deps.remove_interim_file() print(f'Dependency JSON: {deps}') @@ -64,16 +68,209 @@ def test_threaded_scan_dir(self): # with open('scanoss-com.pem', 'rb') as f: # root_certs = f.read() if MyTestCase.TEST_LOCAL: + server_type = 'local' grpc_client = ScanossGrpc(debug=True, url='localhost:50051') else: + server_type = 'remote' grpc_client = ScanossGrpc(debug=True) sc_deps = ScancodeDeps(debug=True) - threaded_deps = ThreadedDependencies(sc_deps, grpc_client, ".", debug=True, trace=True) - self.assertTrue(threaded_deps.run(what_to_scan=".", wait=True)) + threaded_deps = ThreadedDependencies(sc_deps, grpc_client, '.', debug=True, trace=True) + self.assertTrue(threaded_deps.run(what_to_scan='.', wait=True)) deps = threaded_deps.responses - print(f'Dependency results: {deps}') + print(f'Dependency results ({server_type}): {deps}') self.assertIsNotNone(deps) + def test_dep_scope_all(self): + """ + Run a dependency scan of the current directory, then parse those results + """ + # with open('scanoss-com.pem', 'rb') as f: + # root_certs = f.read() + if MyTestCase.TEST_LOCAL: + server_type = 'local' + grpc_client = ScanossGrpc(debug=True, url='localhost:50051') + else: + server_type = 'remote' + grpc_client = ScanossGrpc(debug=True) + sc_deps = ScancodeDeps(debug=True) + threaded_deps = ThreadedDependencies(sc_deps, grpc_client, '.', debug=True, trace=True) + self.assertTrue(threaded_deps.run(what_to_scan='.', wait=True)) + deps = threaded_deps.responses + files = deps.get('files') + package_json_deps = files[0]['dependencies'] + requirements_txt_deps = files[1].get('dependencies', []) + print(f'Dependency results for: ({files[0]["file"]}), dependencies: {package_json_deps}') + print(f'Dependency results for: ({files[1]["file"]}), dependencies: {requirements_txt_deps}') + self.assertEqual(len(package_json_deps), 3) + self.assertEqual(len(requirements_txt_deps), 6) + + def test_dep_scope_development(self): + """ + Run a dependency scan of the current directory, then parse those results + """ + # with open('scanoss-com.pem', 'rb') as f: + # root_certs = f.read() + if MyTestCase.TEST_LOCAL: + server_type = 'local' + grpc_client = ScanossGrpc(debug=True, url='localhost:50051') + else: + server_type = 'remote' + grpc_client = ScanossGrpc(debug=True) + sc_deps = ScancodeDeps(debug=True) + threaded_deps = ThreadedDependencies(sc_deps, grpc_client, '.', debug=True, trace=True) + self.assertTrue(threaded_deps.run(what_to_scan='.', wait=True, dep_scope=SCOPE.DEVELOPMENT)) + deps = threaded_deps.responses + files = deps.get('files') + package_json_dev_deps = files[0]['dependencies'] + requirements_txt_dev_deps = files[1].get('dependencies', []) + print(f'Dependency results for: ({files[0]["file"]}), dependencies: {package_json_dev_deps}') + print(f'Dependency results for: ({files[1]["file"]}), dependencies: {requirements_txt_dev_deps}') + self.assertNotEquals(len(package_json_dev_deps), len(requirements_txt_dev_deps)) + self.assertEqual(len(package_json_dev_deps), 1) + # devDependencies of package.json file: "@babel/core": ">0.2.0" + self.assertEqual(package_json_dev_deps[0]['component'], '@babel/core') + + def test_dep_scope_production(self): + """ + Run a dependency scan of the current directory, then parse those results + """ + # with open('scanoss-com.pem', 'rb') as f: + # root_certs = f.read() + if MyTestCase.TEST_LOCAL: + server_type = 'local' + grpc_client = ScanossGrpc(debug=True, url='localhost:50051') + else: + server_type = 'remote' + grpc_client = ScanossGrpc(debug=True) + sc_deps = ScancodeDeps(debug=True) + threaded_deps = ThreadedDependencies(sc_deps, grpc_client, '.', debug=True, trace=True) + self.assertTrue(threaded_deps.run(what_to_scan='.', wait=True, dep_scope=SCOPE.PRODUCTION)) + deps = threaded_deps.responses + files = deps.get('files') + package_json_deps = files[0]['dependencies'] + requirements_txt_deps = files[1].get('dependencies', []) + print(f'Dependency results for: ({files[0]["file"]}), dependencies: {package_json_deps}') + print(f'Dependency results for: ({files[1]["file"]}), dependencies: {requirements_txt_deps}') + + self.assertNotEquals(len(requirements_txt_deps), 5) + self.assertEqual(len(package_json_deps), 2) + + self.assertEqual(package_json_deps[0]['component'], 'uuid') + self.assertEqual(package_json_deps[1]['component'], 'xml-js') + + def test_dep_scope_include(self): + """ + Run a dependency scan of the current directory, then parse those results + """ + # with open('scanoss-com.pem', 'rb') as f: + # root_certs = f.read() + if MyTestCase.TEST_LOCAL: + server_type = 'local' + grpc_client = ScanossGrpc(debug=True, url='localhost:50051') + else: + server_type = 'remote' + grpc_client = ScanossGrpc(debug=True) + sc_deps = ScancodeDeps(debug=True) + threaded_deps = ThreadedDependencies(sc_deps, grpc_client, '.', debug=True, trace=True) + self.assertTrue(threaded_deps.run(what_to_scan='.', wait=True, dep_scope_include='dependencies')) + deps = threaded_deps.responses + files = deps.get('files') + package_json_deps = files[0]['dependencies'] + requirements_txt_deps = files[1].get('dependencies', []) + print(f'Dependency results for: ({files[0]["file"]}), dependencies: {package_json_deps}') + print(f'Dependency results for: ({files[1]["file"]}), dependencies: {requirements_txt_deps}') + + # requirements.txt dependencies should be empty due to the filter 'dependencies' + self.assertEqual(len(requirements_txt_deps), 0) + self.assertEqual(len(package_json_deps), 2) + # Prod dependencies package.json file: "uuid" and "xml-js" + self.assertEqual(package_json_deps[0]['component'], 'uuid') + self.assertEqual(package_json_deps[1]['component'], 'xml-js') + + def test_dep_scope_exclude(self): + """ + Run a dependency scan of the current directory, then parse those results + """ + # with open('scanoss-com.pem', 'rb') as f: + # root_certs = f.read() + if MyTestCase.TEST_LOCAL: + server_type = 'local' + grpc_client = ScanossGrpc(debug=True, url='localhost:50051') + else: + server_type = 'remote' + grpc_client = ScanossGrpc(debug=True) + sc_deps = ScancodeDeps(debug=True) + threaded_deps = ThreadedDependencies(sc_deps, grpc_client, '.', debug=True, trace=True) + self.assertTrue(threaded_deps.run(what_to_scan='.', wait=True, dep_scope_exclude='dependencies,install')) + deps = threaded_deps.responses + files = deps.get('files') + package_json_deps = files[0]['dependencies'] + requirements_txt_deps = files[1].get('dependencies', []) + print(f'Dependency results for: ({files[0]["file"]}), dependencies: {package_json_deps}') + print(f'Dependency results for: ({files[1]["file"]}), dependencies: {requirements_txt_deps}') + self.assertEqual(len(requirements_txt_deps), 0) + + ## Only dev dependencies should be presents because 'dependencies' and 'install' scopes are excluded + self.assertEqual(len(package_json_deps), 1) + + # Prod dependencies package.json file: "uuid" and "xml-js" + self.assertEqual(package_json_deps[0]['component'], '@babel/core') + + def test_dep_scope_override(self): + """ + Run a dependency scan of the current directory, then parse those results + """ + # with open('scanoss-com.pem', 'rb') as f: + # root_certs = f.read() + if MyTestCase.TEST_LOCAL: + server_type = 'local' + grpc_client = ScanossGrpc(debug=True, url='localhost:50051') + else: + server_type = 'remote' + grpc_client = ScanossGrpc(debug=True) + sc_deps = ScancodeDeps(debug=True) + threaded_deps = ThreadedDependencies(sc_deps, grpc_client, '.', debug=True, trace=True) + self.assertTrue( + threaded_deps.run( + what_to_scan='.', wait=True, dep_scope=SCOPE.PRODUCTION, dep_scope_exclude='dependencies,install' + ) + ) + deps = threaded_deps.responses + files = deps.get('files') + package_json_deps = files[0]['dependencies'] + requirements_txt_deps = files[1].get('dependencies', []) + print(f'Dependency results for: ({files[0]["file"]}), dependencies: {package_json_deps}') + print(f'Dependency results for: ({files[1]["file"]}), dependencies: {requirements_txt_deps}') + self.assertEqual(len(requirements_txt_deps), 0) + + ## Only dev dependencies should be presents because 'dependencies' and 'install' scopes are excluded + self.assertEqual(len(package_json_deps), 1) + + # Prod dependencies package.json file: "uuid" and "xml-js" + self.assertEqual(package_json_deps[0]['component'], '@babel/core') + + def test_dependency_scan(self): + """ + Run a dependency scan of the current directory. Dependencies should be returned without scopes + """ + temp_dir = tempfile.gettempdir() + file_name = 'dependency-result-output.json' + output_file = os.path.join(temp_dir, file_name) + sc_deps = ScancodeDeps(debug=True, trace=True) + + success = sc_deps.get_dependencies(what_to_scan='.', result_output=output_file) + self.assertTrue(success) + with open(output_file, 'r') as result: + # Parse the JSON data from the file + dependencies = json.load(result) + files = dependencies.get('files') + for file in files: + purls = file.get('purls') + contains_scope = any('scope' in purl for purl in purls) + self.assertFalse(contains_scope) + + os.remove(output_file) + if __name__ == '__main__': unittest.main() diff --git a/tests/scanossapi-test.py b/tests/scanossapi-test.py new file mode 100644 index 00000000..e28e4556 --- /dev/null +++ b/tests/scanossapi-test.py @@ -0,0 +1,38 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +import unittest +from scanoss.scanossapi import ScanossApi + +class MyTestCase(unittest.TestCase): + + def test_scanoss_generic_headers(self): + scanoss_api = ScanossApi(debug=True, req_headers={'x-api-key': '123455', + 'generic-header': 'generic-header-value'}) + required_keys = ('x-api-key', 'X-Session', 'User-Agent', 'user-agent', 'generic-header') + valid_headers = True + for key, value in scanoss_api.headers.items(): + if key not in required_keys: + valid_headers = False + self.assertTrue(valid_headers) \ No newline at end of file diff --git a/tests/test_bom_path_matching.py b/tests/test_bom_path_matching.py new file mode 100644 index 00000000..ae0db00c --- /dev/null +++ b/tests/test_bom_path_matching.py @@ -0,0 +1,1076 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2024, SCANOSS + + 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. +""" + +import json +import os +import shutil +import tempfile +import unittest +from pathlib import Path +from unittest.mock import MagicMock + +from scanoss.scanner import Scanner +from scanoss.scanoss_settings import ( + BomEntry, + ReplaceRule, + SbomContext, + ScanossSettings, + find_best_match, +) +from scanoss.scanpostprocessor import ScanPostProcessor + + +class TestMatchesPath(unittest.TestCase): + """Unit tests for the BomEntry.matches_path method""" + + def test_empty_entry_path_matches_everything(self): + self.assertTrue(BomEntry(path='').matches_path('src/main.c')) + self.assertTrue(BomEntry(path=None).matches_path('src/main.c')) + self.assertTrue(BomEntry(path='').matches_path('')) + + def test_exact_file_match(self): + self.assertTrue(BomEntry(path='src/main.c').matches_path('src/main.c')) + + def test_exact_file_no_match(self): + self.assertFalse(BomEntry(path='src/main.c').matches_path('src/other.c')) + + def test_folder_prefix_match(self): + self.assertTrue(BomEntry(path='src/vendor/').matches_path('src/vendor/lib.c')) + self.assertTrue(BomEntry(path='src/vendor/').matches_path('src/vendor/sub/deep.c')) + + def test_folder_no_match(self): + self.assertFalse(BomEntry(path='src/vendor/').matches_path('src/other/lib.c')) + self.assertFalse(BomEntry(path='src/vendor/').matches_path('src/vendorlib.c')) + + def test_folder_root_prefix(self): + self.assertTrue(BomEntry(path='src/').matches_path('src/main.c')) + self.assertTrue(BomEntry(path='src/').matches_path('src/vendor/deep/file.c')) + + def test_folder_without_trailing_slash(self): + """Paths without trailing slash should still do prefix matching""" + self.assertTrue(BomEntry(path='src/vendor').matches_path('src/vendor/lib.c')) + self.assertTrue(BomEntry(path='src/vendor').matches_path('src/vendor/sub/deep.c')) + + def test_folder_without_trailing_slash_no_match(self): + self.assertFalse(BomEntry(path='src/vendor').matches_path('src/other/lib.c')) + self.assertFalse(BomEntry(path='src/vendor').matches_path('src/vendorlib.c')) + + def test_folder_without_trailing_slash_exact_match(self): + """Path without trailing slash should still match the exact path""" + self.assertTrue(BomEntry(path='src/vendor').matches_path('src/vendor')) + + def test_exact_path_does_not_prefix_match(self): + """File paths should not do prefix matching on partial names""" + self.assertFalse(BomEntry(path='src/main.c').matches_path('src/main.cpp')) + + +class TestFromDictNormalization(unittest.TestCase): + """Unit tests for trailing-slash normalization in from_dict""" + + def test_bom_entry_strips_trailing_slash(self): + entry = BomEntry.from_dict({'path': 'src/vendor/'}) + self.assertEqual(entry.path, 'src/vendor') + + def test_bom_entry_no_trailing_slash_unchanged(self): + entry = BomEntry.from_dict({'path': 'src/main.c'}) + self.assertEqual(entry.path, 'src/main.c') + + def test_bom_entry_none_path(self): + entry = BomEntry.from_dict({}) + self.assertIsNone(entry.path) + + def test_replace_rule_strips_trailing_slash(self): + entry = ReplaceRule.from_dict({ + 'path': 'src/vendor/', + 'purl': 'pkg:npm/old', + 'replace_with': 'pkg:npm/new', + }) + self.assertEqual(entry.path, 'src/vendor') + + def test_replace_rule_no_trailing_slash_unchanged(self): + entry = ReplaceRule.from_dict({ + 'path': 'src/main.c', + 'purl': 'pkg:npm/old', + 'replace_with': 'pkg:npm/new', + }) + self.assertEqual(entry.path, 'src/main.c') + + +class TestEntryPriority(unittest.TestCase): + """Unit tests for the BomEntry.priority property""" + + def test_path_and_purl(self): + self.assertEqual(BomEntry(path='src/main.c', purl='pkg:npm/vue').priority, 4) + + def test_purl_only(self): + self.assertEqual(BomEntry(purl='pkg:npm/vue').priority, 2) + + def test_path_only(self): + self.assertEqual(BomEntry(path='src/vendor/').priority, 1) + + def test_empty_entry(self): + self.assertEqual(BomEntry().priority, 0) + + def test_empty_strings(self): + self.assertEqual(BomEntry(path='', purl='').priority, 0) + + +class TestFindBestMatch(unittest.TestCase): + """Unit tests for the find_best_match helper function""" + + def test_no_entries(self): + result = find_best_match('src/main.c', ['pkg:npm/vue'], []) + self.assertIsNone(result) + + def test_purl_only_match(self): + entries = [BomEntry(purl='pkg:npm/vue')] + result = find_best_match('src/main.c', ['pkg:npm/vue'], entries) + self.assertEqual(result, entries[0]) + + def test_path_only_match(self): + entries = [BomEntry(path='src/vendor/')] + result = find_best_match('src/vendor/lib.c', ['pkg:npm/vue'], entries) + self.assertEqual(result, entries[0]) + + def test_full_match_beats_purl_only(self): + entries = [ + BomEntry(purl='pkg:npm/vue'), + BomEntry(path='src/main.c', purl='pkg:npm/vue'), + ] + result = find_best_match('src/main.c', ['pkg:npm/vue'], entries) + self.assertEqual(result, entries[1]) + + def test_full_match_beats_path_only(self): + entries = [ + BomEntry(path='src/'), + BomEntry(path='src/main.c', purl='pkg:npm/vue'), + ] + result = find_best_match('src/main.c', ['pkg:npm/vue'], entries) + self.assertEqual(result, entries[1]) + + def test_longer_path_wins_on_tie(self): + entries = [ + BomEntry(path='src/', purl='pkg:npm/vue'), + BomEntry(path='src/vendor/', purl='pkg:npm/vue'), + ] + result = find_best_match('src/vendor/lib.c', ['pkg:npm/vue'], entries) + self.assertEqual(result, entries[1]) + + def test_no_match_when_purl_not_in_result(self): + entries = [BomEntry(purl='pkg:npm/react')] + result = find_best_match('src/main.c', ['pkg:npm/vue'], entries) + self.assertIsNone(result) + + def test_no_match_when_path_does_not_match(self): + entries = [BomEntry(path='lib/', purl='pkg:npm/vue')] + result = find_best_match('src/main.c', ['pkg:npm/vue'], entries) + self.assertIsNone(result) + + def test_path_only_entry_matches_without_purl(self): + """Path-only remove entries should match regardless of result purls""" + entries = [BomEntry(path='src/vendor/')] + result = find_best_match('src/vendor/lib.c', [], entries) + self.assertEqual(result, entries[0]) + + def test_skip_entries_with_no_path_and_no_purl(self): + entries = [BomEntry(comment='just a comment')] + result = find_best_match('src/main.c', ['pkg:npm/vue'], entries) + self.assertIsNone(result) + + def test_order_independent(self): + """Best match should be found regardless of entry order""" + entries_a = [ + BomEntry(purl='pkg:npm/vue'), + BomEntry(path='src/main.c', purl='pkg:npm/vue'), + ] + entries_b = [ + BomEntry(path='src/main.c', purl='pkg:npm/vue'), + BomEntry(purl='pkg:npm/vue'), + ] + result_a = find_best_match('src/main.c', ['pkg:npm/vue'], entries_a) + result_b = find_best_match('src/main.c', ['pkg:npm/vue'], entries_b) + self.assertEqual(result_a.path, 'src/main.c') + self.assertEqual(result_b.path, 'src/main.c') + + +class TestPostProcessorFolderMatching(unittest.TestCase): + """Test folder-level matching in the post-processor (remove and replace)""" + + def _make_settings(self, settings_data: dict) -> ScanossSettings: + """Create a ScanossSettings instance from a dict without file I/O""" + settings = ScanossSettings() + settings.data = settings_data + return settings + + def test_remove_by_folder_path(self): + """Should remove all results under a folder path""" + settings = self._make_settings({ + 'bom': { + 'remove': [{'path': 'src/vendor/', 'purl': 'pkg:npm/vue'}], + } + }) + results = { + 'src/vendor/lib.c': [{'purl': ['pkg:npm/vue']}], + 'src/vendor/sub/deep.c': [{'purl': ['pkg:npm/vue']}], + 'src/main.c': [{'purl': ['pkg:npm/vue']}], + } + processor = ScanPostProcessor(settings) + processed = processor.load_results(results).post_process() + self.assertNotIn('src/vendor/lib.c', processed) + self.assertNotIn('src/vendor/sub/deep.c', processed) + self.assertIn('src/main.c', processed) + + def test_remove_by_path_only(self): + """Should remove by path only (no purl required)""" + settings = self._make_settings({ + 'bom': { + 'remove': [{'path': 'src/vendor/'}], + } + }) + results = { + 'src/vendor/lib.c': [{'purl': ['pkg:npm/vue']}], + 'src/main.c': [{'purl': ['pkg:npm/react']}], + } + processor = ScanPostProcessor(settings) + processed = processor.load_results(results).post_process() + self.assertNotIn('src/vendor/lib.c', processed) + self.assertIn('src/main.c', processed) + + def test_replace_by_folder_path(self): + """Should replace purls for all results under a folder""" + settings = self._make_settings({ + 'bom': { + 'replace': [{ + 'path': 'src/vendor/', + 'purl': 'pkg:npm/old-lib', + 'replace_with': 'pkg:npm/new-lib', + }], + } + }) + results = { + 'src/vendor/file.c': [{'purl': ['pkg:npm/old-lib']}], + 'src/vendor/sub/deep.c': [{'purl': ['pkg:npm/old-lib']}], + 'src/main.c': [{'purl': ['pkg:npm/old-lib']}], + } + processor = ScanPostProcessor(settings) + processed = processor.load_results(results).post_process() + self.assertEqual(processed['src/vendor/file.c'][0]['purl'], ['pkg:npm/new-lib']) + self.assertEqual(processed['src/vendor/sub/deep.c'][0]['purl'], ['pkg:npm/new-lib']) + self.assertEqual(processed['src/main.c'][0]['purl'], ['pkg:npm/old-lib']) + + def test_priority_specific_file_beats_folder(self): + """A file+purl rule should take priority over a folder+purl rule""" + settings = self._make_settings({ + 'bom': { + 'replace': [ + { + 'path': 'src/vendor/', + 'purl': 'pkg:npm/lib', + 'replace_with': 'pkg:npm/folder-replacement', + }, + { + 'path': 'src/vendor/special.c', + 'purl': 'pkg:npm/lib', + 'replace_with': 'pkg:npm/file-replacement', + }, + ], + } + }) + results = { + 'src/vendor/special.c': [{'purl': ['pkg:npm/lib']}], + 'src/vendor/other.c': [{'purl': ['pkg:npm/lib']}], + } + processor = ScanPostProcessor(settings) + processed = processor.load_results(results).post_process() + # File rule (score 4, longer path) should beat folder rule (score 4, shorter path) + self.assertEqual(processed['src/vendor/special.c'][0]['purl'], ['pkg:npm/file-replacement']) + self.assertEqual(processed['src/vendor/other.c'][0]['purl'], ['pkg:npm/folder-replacement']) + + def test_priority_purl_plus_path_beats_purl_only(self): + """A purl+path rule should take priority over a purl-only rule""" + settings = self._make_settings({ + 'bom': { + 'remove': [ + {'purl': 'pkg:npm/lib'}, # purl-only, score 2 - should NOT match + ], + 'replace': [ + { + 'path': 'src/', + 'purl': 'pkg:npm/lib', + 'replace_with': 'pkg:npm/replacement', + }, + ], + } + }) + # Remove and replace operate independently on results + results = { + 'src/main.c': [{'purl': ['pkg:npm/lib']}], + } + processor = ScanPostProcessor(settings) + processed = processor.load_results(results).post_process() + # The purl-only remove rule matches, so it should be removed + self.assertNotIn('src/main.c', processed) + + def test_deeper_folder_wins(self): + """A deeper folder rule should take priority over a shallower one""" + settings = self._make_settings({ + 'bom': { + 'replace': [ + { + 'path': 'src/', + 'purl': 'pkg:npm/lib', + 'replace_with': 'pkg:npm/shallow-replacement', + }, + { + 'path': 'src/vendor/', + 'purl': 'pkg:npm/lib', + 'replace_with': 'pkg:npm/deep-replacement', + }, + ], + } + }) + results = { + 'src/vendor/file.c': [{'purl': ['pkg:npm/lib']}], + 'src/main.c': [{'purl': ['pkg:npm/lib']}], + } + processor = ScanPostProcessor(settings) + processed = processor.load_results(results).post_process() + self.assertEqual(processed['src/vendor/file.c'][0]['purl'], ['pkg:npm/deep-replacement']) + self.assertEqual(processed['src/main.c'][0]['purl'], ['pkg:npm/shallow-replacement']) + + +class TestSbomForBatch(unittest.TestCase): + """Test per-file SBOM context resolution and payload building""" + + def _make_settings(self, settings_data: dict) -> ScanossSettings: + """Create a ScanossSettings instance from a dict without file I/O""" + settings = ScanossSettings() + settings.data = settings_data + return settings + + # -- get_sbom_context tests -- + + def test_get_sbom_context_purl_only(self): + """Purl-only entries should match any file path""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'purl': 'pkg:npm/global'}, + {'purl': 'pkg:npm/other'}, + ], + } + }) + context = settings.get_sbom_context('anything/file.c') + self.assertIsInstance(context, SbomContext) + self.assertEqual(set(context.purls), {'pkg:npm/global', 'pkg:npm/other'}) + self.assertEqual(context.scan_type, 'identify') + + def test_get_sbom_context_folder_scoped(self): + """Folder-scoped entries should only match files under that folder""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'purl': 'pkg:npm/global'}, + {'path': 'src/vendor/', 'purl': 'pkg:npm/vendor-lib'}, + ], + } + }) + # File under vendor/ gets both purls + context_vendor = settings.get_sbom_context('src/vendor/lib.c') + self.assertEqual(set(context_vendor.purls), {'pkg:npm/global', 'pkg:npm/vendor-lib'}) + self.assertEqual(context_vendor.scan_type, 'identify') + # File outside vendor/ gets only global + context_other = settings.get_sbom_context('src/core/main.c') + self.assertEqual(context_other.purls, ('pkg:npm/global',)) + + def test_get_sbom_context_no_data(self): + """Should return empty SbomContext when no data""" + settings = self._make_settings({}) + context = settings.get_sbom_context('src/main.c') + self.assertEqual(context.purls, ()) + self.assertIsNone(context.scan_type) + + def test_get_sbom_context_no_entries(self): + """Should return empty SbomContext when no include/exclude entries""" + settings = self._make_settings({ + 'bom': { + 'include': [], + 'exclude': [], + } + }) + context = settings.get_sbom_context('src/main.c') + self.assertEqual(context.purls, ()) + self.assertIsNone(context.scan_type) + + def test_get_sbom_context_deduplicates(self): + """Should not duplicate purls when multiple entries match same purl""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'purl': 'pkg:npm/vue'}, + {'path': 'src/', 'purl': 'pkg:npm/vue'}, + ], + } + }) + context = settings.get_sbom_context('src/main.c') + self.assertEqual(context.purls.count('pkg:npm/vue'), 1) + + def test_get_sbom_context_ordered_by_specificity(self): + """Should return purls ordered by specificity (most specific first)""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'purl': 'pkg:npm/global'}, # score: 2 (purl only) + {'path': 'src/', 'purl': 'pkg:npm/src-lib'}, # score: 4 + 3 = 7 (normalized to 'src') + {'path': 'src/vendor/', 'purl': 'pkg:npm/vendor-lib'}, # score: 4 + 10 = 14 (normalized to 'src/vendor') + ], + } + }) + context = settings.get_sbom_context('src/vendor/lib.c') + # Most specific first: vendor-lib (14), src-lib (7), global (2) + self.assertEqual(context.purls, ('pkg:npm/vendor-lib', 'pkg:npm/src-lib', 'pkg:npm/global')) + + def test_get_sbom_context_file_path_most_specific(self): + """File path should be more specific than folder path""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'path': 'src/', 'purl': 'pkg:npm/folder-lib'}, + {'path': 'src/main.c', 'purl': 'pkg:npm/file-lib'}, + ], + } + }) + context = settings.get_sbom_context('src/main.c') + # File path (10 chars) more specific than folder path (4 chars) + self.assertEqual(context.purls[0], 'pkg:npm/file-lib') + + # -- SbomContext.to_payload tests -- + + def test_sbom_context_to_payload_identify(self): + """Should return identify scan type when scan_type is 'identify'""" + context = SbomContext(purls=('pkg:npm/vue', 'pkg:npm/react'), scan_type='identify') + result = context.to_payload() + self.assertIsNotNone(result) + self.assertEqual(result['scan_type'], 'identify') + assets = json.loads(result['assets']) + # Order should be preserved + self.assertEqual(assets['components'], [{'purl': 'pkg:npm/vue'}, {'purl': 'pkg:npm/react'}]) + + def test_sbom_context_to_payload_blacklist(self): + """Should return blacklist scan type when scan_type is 'blacklist'""" + context = SbomContext(purls=('pkg:npm/excluded',), scan_type='blacklist') + result = context.to_payload() + self.assertIsNotNone(result) + self.assertEqual(result['scan_type'], 'blacklist') + + def test_sbom_context_to_payload_empty_purls(self): + """Should return None for empty purls tuple""" + context = SbomContext(purls=(), scan_type='identify') + result = context.to_payload() + self.assertIsNone(result) + + def test_sbom_context_to_payload_none_scan_type(self): + """Should return None when scan_type is None""" + context = SbomContext(purls=('pkg:npm/vue',), scan_type=None) + result = context.to_payload() + self.assertIsNone(result) + + def test_sbom_context_to_payload_preserves_order(self): + """Should preserve the order of purls in the payload""" + purls = ('pkg:npm/c', 'pkg:npm/a', 'pkg:npm/b') + context = SbomContext(purls=purls, scan_type='identify') + result = context.to_payload() + assets = json.loads(result['assets']) + self.assertEqual([c['purl'] for c in assets['components']], list(purls)) + + def test_sbom_context_empty(self): + """SbomContext.empty() should return empty context""" + context = SbomContext.empty() + self.assertEqual(context.purls, ()) + self.assertIsNone(context.scan_type) + self.assertIsNone(context.to_payload()) + + # -- Integration tests (get_sbom_context + to_payload) -- + + def test_folder_scoped_entry_included_when_matching(self): + """Folder-scoped entry should be included when file matches""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'path': 'src/vendor/', 'purl': 'pkg:npm/vue'}, + ], + } + }) + context = settings.get_sbom_context('src/vendor/lib.c') + result = context.to_payload() + self.assertIsNotNone(result) + assets = json.loads(result['assets']) + self.assertEqual([c['purl'] for c in assets['components']], ['pkg:npm/vue']) + + def test_folder_scoped_entry_excluded_when_no_match(self): + """Folder-scoped entry should not be included when file doesn't match""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'path': 'src/vendor/', 'purl': 'pkg:npm/vue'}, + ], + } + }) + context = settings.get_sbom_context('lib/other.c') + result = context.to_payload() + self.assertIsNone(result) + + def test_mixed_purl_only_and_scoped(self): + """Purl-only entries always included, scoped entries filtered by path""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'purl': 'pkg:npm/global-lib'}, + {'path': 'src/vendor/', 'purl': 'pkg:npm/vendor-lib'}, + {'path': 'lib/', 'purl': 'pkg:npm/lib-only'}, + ], + } + }) + context = settings.get_sbom_context('src/vendor/file.c') + self.assertIn('pkg:npm/global-lib', context.purls) + self.assertIn('pkg:npm/vendor-lib', context.purls) + self.assertNotIn('pkg:npm/lib-only', context.purls) + + def test_include_no_match_falls_back_to_exclude(self): + """When include rules exist but don't match, should fall back to exclude rules""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'path': 'vendor/', 'purl': 'pkg:npm/react'}, + ], + 'exclude': [ + {'purl': 'pkg:npm/lodash'}, + ], + } + }) + # File in vendor/ should use include rules + context_vendor = settings.get_sbom_context('vendor/lib.c') + self.assertEqual(context_vendor.purls, ('pkg:npm/react',)) + self.assertEqual(context_vendor.scan_type, 'identify') + # File outside vendor/ should fall back to exclude rules + context_other = settings.get_sbom_context('src/app.js') + self.assertEqual(context_other.purls, ('pkg:npm/lodash',)) + self.assertEqual(context_other.scan_type, 'blacklist') + + # -- replace_with as identify context tests -- + + def test_replace_with_sent_as_identify_context(self): + """Replace rule with matching path sends replace_with PURL as identify context""" + settings = self._make_settings({ + 'bom': { + 'replace': [ + {'path': 'vendor/', 'purl': 'pkg:npm/old-lib', 'replace_with': 'pkg:npm/new-lib'}, + ], + } + }) + context = settings.get_sbom_context('vendor/file.js') + self.assertEqual(context.purls, ('pkg:npm/new-lib',)) + self.assertEqual(context.scan_type, 'identify') + + def test_replace_with_global_sent_for_all_files(self): + """Replace rule without path sends replace_with PURL for any file""" + settings = self._make_settings({ + 'bom': { + 'replace': [ + {'purl': 'pkg:npm/old-lib', 'replace_with': 'pkg:npm/new-lib'}, + ], + } + }) + context_a = settings.get_sbom_context('src/app.js') + self.assertEqual(context_a.purls, ('pkg:npm/new-lib',)) + self.assertEqual(context_a.scan_type, 'identify') + + context_b = settings.get_sbom_context('lib/utils.c') + self.assertEqual(context_b.purls, ('pkg:npm/new-lib',)) + self.assertEqual(context_b.scan_type, 'identify') + + def test_replace_with_merged_with_include(self): + """Both include and replace_with PURLs appear in identify context, deduplicated""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'purl': 'pkg:npm/react'}, + ], + 'replace': [ + {'purl': 'pkg:npm/old-lib', 'replace_with': 'pkg:npm/new-lib'}, + {'purl': 'pkg:npm/dup', 'replace_with': 'pkg:npm/react'}, # duplicate of include + ], + } + }) + context = settings.get_sbom_context('src/app.js') + self.assertEqual(context.scan_type, 'identify') + self.assertIn('pkg:npm/react', context.purls) + self.assertIn('pkg:npm/new-lib', context.purls) + # Deduplicated: react should appear only once + self.assertEqual(context.purls.count('pkg:npm/react'), 1) + + def test_replace_with_no_match_falls_to_exclude(self): + """Path-scoped replace that doesn't match should not interfere with exclude fallback""" + settings = self._make_settings({ + 'bom': { + 'replace': [ + {'path': 'vendor/', 'purl': 'pkg:npm/old-lib', 'replace_with': 'pkg:npm/new-lib'}, + ], + 'exclude': [ + {'purl': 'pkg:npm/lodash'}, + ], + } + }) + context = settings.get_sbom_context('src/app.js') + self.assertEqual(context.purls, ('pkg:npm/lodash',)) + self.assertEqual(context.scan_type, 'blacklist') + + def test_replace_with_overrides_exclude(self): + """When both replace and exclude match, identify context wins""" + settings = self._make_settings({ + 'bom': { + 'replace': [ + {'purl': 'pkg:npm/old-lib', 'replace_with': 'pkg:npm/new-lib'}, + ], + 'exclude': [ + {'purl': 'pkg:npm/lodash'}, + ], + } + }) + context = settings.get_sbom_context('src/app.js') + self.assertEqual(context.purls, ('pkg:npm/new-lib',)) + self.assertEqual(context.scan_type, 'identify') + + def test_replace_missing_replace_with_warns_and_skips(self): + """Replace rule missing replace_with should be skipped with a warning""" + settings = self._make_settings({ + 'bom': { + 'replace': [ + {'purl': 'pkg:npm/bad-rule'}, # missing replace_with + {'purl': 'pkg:npm/old-lib', 'replace_with': 'pkg:npm/new-lib'}, + ], + } + }) + from unittest.mock import patch + with patch.object(settings, 'print_stderr') as mock_stderr: + rules = settings.get_bom_replace() + # Invalid entry filtered out + self.assertEqual(len(rules), 1) + self.assertEqual(rules[0].replace_with, 'pkg:npm/new-lib') + # Warning was printed + mock_stderr.assert_called_once() + self.assertIn('replace_with', mock_stderr.call_args[0][0]) + + +class TestScannerSbomPayload(unittest.TestCase): + """End-to-end tests: verify Scanner sends the correct SBOM payload in HTTP POST requests""" + + def setUp(self): + self.test_dir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.test_dir) + + def _create_files(self, file_paths): + """Create test files in self.test_dir with enough content for WFP generation.""" + for rel_path in file_paths: + abs_path = os.path.join(self.test_dir, rel_path) + os.makedirs(os.path.dirname(abs_path), exist_ok=True) + with open(abs_path, 'w') as f: + f.write('/* generated test content */\n' * 20) + + def _make_settings(self, settings_data): + """Create ScanossSettings from a dict without file I/O.""" + settings = ScanossSettings() + settings.data = settings_data + settings.settings_file_type = 'new' + return settings + + def _create_scanner(self, settings=None): + """Create a Scanner with mocked session.post. + + Returns: + (scanner, mock_post) tuple + """ + scanner = Scanner( + scanoss_settings=settings, + nb_threads=1, + quiet=True, + scan_options=3, # FILES + SNIPPETS, no deps + ) + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.ok = True + mock_response.json.return_value = {} + mock_response.text = '{}' + + mock_post = MagicMock(return_value=mock_response) + scanner.scanoss_api.session.post = mock_post + + return scanner, mock_post + + def _extract_payloads(self, mock_post): + """Extract form_data dicts from all session.post calls.""" + payloads = [] + for call in mock_post.call_args_list: + form_data = call.kwargs.get('data', {}) + payloads.append(form_data) + return payloads + + # -- SBOM tests: purl-only entries -- + + def test_sbom_include_sent_in_post(self): + """When purl-only include entries exist, every POST should contain + type='identify' and the correct purls in assets.""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'purl': 'pkg:npm/vue@2.6.12'}, + {'purl': 'pkg:npm/react@17.0.0'}, + ], + } + }) + self._create_files(['src/main.c', 'src/lib.c']) + scanner, mock_post = self._create_scanner(settings) + + scanner.scan_folder_with_options(self.test_dir) + + self.assertTrue(mock_post.called, 'Expected at least one POST call') + payloads = self._extract_payloads(mock_post) + for payload in payloads: + self.assertEqual(payload.get('type'), 'identify') + assets = json.loads(payload.get('assets')) + purls = {c['purl'] for c in assets['components']} + self.assertIn('pkg:npm/vue@2.6.12', purls) + self.assertIn('pkg:npm/react@17.0.0', purls) + + def test_sbom_exclude_sent_as_blacklist(self): + """When purl-only exclude entries exist, every POST should contain + type='blacklist' and the correct purls in assets.""" + settings = self._make_settings({ + 'bom': { + 'exclude': [ + {'purl': 'pkg:npm/unwanted@1.0.0'}, + ], + } + }) + self._create_files(['src/main.c']) + scanner, mock_post = self._create_scanner(settings) + + scanner.scan_folder_with_options(self.test_dir) + + self.assertTrue(mock_post.called, 'Expected at least one POST call') + payloads = self._extract_payloads(mock_post) + for payload in payloads: + self.assertEqual(payload.get('type'), 'blacklist') + assets = json.loads(payload.get('assets')) + purls = {c['purl'] for c in assets['components']} + self.assertIn('pkg:npm/unwanted@1.0.0', purls) + + def test_no_bom_entries_no_sbom_in_payload(self): + """When settings have empty BOM lists, POST should have no type/assets.""" + settings = self._make_settings({ + 'bom': { + 'include': [], + 'exclude': [], + } + }) + self._create_files(['src/main.c']) + scanner, mock_post = self._create_scanner(settings) + + scanner.scan_folder_with_options(self.test_dir) + + self.assertTrue(mock_post.called, 'Expected at least one POST call') + payloads = self._extract_payloads(mock_post) + for payload in payloads: + self.assertNotIn('type', payload) + self.assertNotIn('assets', payload) + + # -- SBOM tests: path-scoped entries -- + + def test_sbom_path_scoped_include(self): + """Path-scoped include: files with different contexts should be + sent in separate batches with the correct purls each.""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'purl': 'pkg:npm/global-lib'}, + {'path': 'src/vendor/', 'purl': 'pkg:npm/vendor-lib'}, + ], + } + }) + self._create_files(['src/vendor/lib.c', 'src/main.c']) + scanner, mock_post = self._create_scanner(settings) + + scanner.scan_folder_with_options(self.test_dir) + + self.assertTrue(mock_post.called, 'Expected at least one POST call') + payloads = self._extract_payloads(mock_post) + # Context-change flush splits into 2 batches: + # - vendor batch: {global-lib, vendor-lib} + # - non-vendor batch: {global-lib} only + self.assertEqual(len(payloads), 2, + f'Expected 2 POST calls (different contexts), got {len(payloads)}') + + purl_sets = [] + for payload in payloads: + self.assertEqual(payload.get('type'), 'identify') + assets = json.loads(payload.get('assets')) + purls = frozenset(c['purl'] for c in assets['components']) + purl_sets.append(purls) + + self.assertIn(frozenset({'pkg:npm/global-lib', 'pkg:npm/vendor-lib'}), purl_sets) + self.assertIn(frozenset({'pkg:npm/global-lib'}), purl_sets) + + def test_sbom_no_matching_paths(self): + """Path-scoped include with no matching files: POST should have no type/assets.""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'path': 'vendor/', 'purl': 'pkg:npm/vendor-only'}, + ], + } + }) + # Files are NOT under vendor/ + self._create_files(['src/main.c']) + scanner, mock_post = self._create_scanner(settings) + + scanner.scan_folder_with_options(self.test_dir) + + self.assertTrue(mock_post.called, 'Expected at least one POST call') + payloads = self._extract_payloads(mock_post) + for payload in payloads: + self.assertNotIn('type', payload) + self.assertNotIn('assets', payload) + + def test_sbom_exclude_path_scoped(self): + """Path-scoped exclude: matching batch should contain type='blacklist'.""" + settings = self._make_settings({ + 'bom': { + 'exclude': [ + {'path': 'src/', 'purl': 'pkg:npm/blocked'}, + ], + } + }) + self._create_files(['src/main.c']) + scanner, mock_post = self._create_scanner(settings) + + scanner.scan_folder_with_options(self.test_dir) + + self.assertTrue(mock_post.called, 'Expected at least one POST call') + payloads = self._extract_payloads(mock_post) + for payload in payloads: + self.assertEqual(payload.get('type'), 'blacklist') + assets = json.loads(payload.get('assets')) + purls = {c['purl'] for c in assets['components']} + self.assertIn('pkg:npm/blocked', purls) + + # -- Context-change flush tests -- + + def test_context_change_flushes_batch(self): + """Files in folders with different path-scoped purls should be + sent in separate POST requests with only their matching purls.""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'purl': 'pkg:npm/global-lib'}, + {'path': 'src/vendor/', 'purl': 'pkg:npm/vendor-lib'}, + {'path': 'src/core/', 'purl': 'pkg:npm/core-lib'}, + ], + } + }) + self._create_files([ + 'src/vendor/a.c', + 'src/core/b.c', + ]) + scanner, mock_post = self._create_scanner(settings) + + scanner.scan_folder_with_options(self.test_dir) + + self.assertTrue(mock_post.called, 'Expected at least one POST call') + payloads = self._extract_payloads(mock_post) + + # Should have exactly 2 POST requests (one per folder context) + self.assertEqual(len(payloads), 2, + f'Expected exactly 2 POST calls (one per folder context), got {len(payloads)}') + + # Collect the purl sets from each payload + purl_sets = [] + for payload in payloads: + self.assertEqual(payload.get('type'), 'identify') + assets = json.loads(payload.get('assets')) + purls = frozenset(c['purl'] for c in assets['components']) + purl_sets.append(purls) + + # One payload should have {global, vendor}, the other {global, core} + expected_vendor = frozenset({'pkg:npm/global-lib', 'pkg:npm/vendor-lib'}) + expected_core = frozenset({'pkg:npm/global-lib', 'pkg:npm/core-lib'}) + + self.assertIn(expected_vendor, purl_sets, + f'Expected vendor payload with {expected_vendor}, got {purl_sets}') + self.assertIn(expected_core, purl_sets, + f'Expected core payload with {expected_core}, got {purl_sets}') + + # -- No settings test -- + + def test_no_settings_no_sbom_in_payload(self): + """When Scanner has no scan_settings, POST should have no type/assets.""" + self._create_files(['src/main.c']) + scanner, mock_post = self._create_scanner(settings=None) + + scanner.scan_folder_with_options(self.test_dir) + + self.assertTrue(mock_post.called, 'Expected at least one POST call') + payloads = self._extract_payloads(mock_post) + for payload in payloads: + self.assertNotIn('type', payload) + self.assertNotIn('assets', payload) + + # -- Corner case tests: priority ordering -- + + def test_payload_preserves_specificity_order(self): + """Purls in payload should be ordered by specificity (most specific first).""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'purl': 'pkg:npm/global'}, # score: 2 + {'path': 'src/', 'purl': 'pkg:npm/src-lib'}, # score: 4 + 3 = 7 (normalized to 'src') + {'path': 'src/vendor/', 'purl': 'pkg:npm/vendor'}, # score: 4 + 10 = 14 (normalized to 'src/vendor') + ], + } + }) + self._create_files(['src/vendor/lib.c']) + scanner, mock_post = self._create_scanner(settings) + scanner.scan_folder_with_options(self.test_dir) + + payloads = self._extract_payloads(mock_post) + self.assertEqual(len(payloads), 1) + assets = json.loads(payloads[0]['assets']) + purl_order = [c['purl'] for c in assets['components']] + # Most specific first + self.assertEqual(purl_order, ['pkg:npm/vendor', 'pkg:npm/src-lib', 'pkg:npm/global']) + + def test_same_context_batches_together(self): + """Files with identical purl context should batch into single POST.""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'purl': 'pkg:npm/global'}, + {'path': 'src/', 'purl': 'pkg:npm/src-lib'}, + ], + } + }) + # All files under src/ → same context + self._create_files(['src/a.c', 'src/b.c', 'src/c.c']) + scanner, mock_post = self._create_scanner(settings) + scanner.scan_folder_with_options(self.test_dir) + + payloads = self._extract_payloads(mock_post) + self.assertEqual(len(payloads), 1, 'Expected single POST for same context') + + def test_nested_folder_deeper_wins(self): + """Deeper folder rule should produce higher-priority purl in payload.""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'path': 'src/', 'purl': 'pkg:npm/shallow'}, + {'path': 'src/vendor/', 'purl': 'pkg:npm/deep'}, + ], + } + }) + self._create_files(['src/vendor/lib.c']) + scanner, mock_post = self._create_scanner(settings) + scanner.scan_folder_with_options(self.test_dir) + + payloads = self._extract_payloads(mock_post) + self.assertEqual(len(payloads), 1) + assets = json.loads(payloads[0]['assets']) + purl_order = [c['purl'] for c in assets['components']] + # deep (score 14) before shallow (score 7) + self.assertEqual(purl_order[0], 'pkg:npm/deep') + + def test_file_path_beats_folder_path_ordering(self): + """File-specific rule should appear before folder rule for ordering.""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'path': 'src/', 'purl': 'pkg:npm/folder-lib'}, # score: 8 + {'path': 'src/main.c', 'purl': 'pkg:npm/file-lib'}, # score: 14 + ], + } + }) + self._create_files(['src/main.c']) + scanner, mock_post = self._create_scanner(settings) + scanner.scan_folder_with_options(self.test_dir) + + payloads = self._extract_payloads(mock_post) + self.assertEqual(len(payloads), 1) + assets = json.loads(payloads[0]['assets']) + purl_order = [c['purl'] for c in assets['components']] + self.assertEqual(purl_order[0], 'pkg:npm/file-lib') + + def test_three_contexts_three_posts(self): + """Files in 3 different folder contexts should result in 3 POSTs.""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'path': 'vendor/', 'purl': 'pkg:npm/vendor'}, + {'path': 'core/', 'purl': 'pkg:npm/core'}, + {'path': 'lib/', 'purl': 'pkg:npm/lib'}, + ], + } + }) + self._create_files(['vendor/a.c', 'core/b.c', 'lib/c.c']) + scanner, mock_post = self._create_scanner(settings) + scanner.scan_folder_with_options(self.test_dir) + + payloads = self._extract_payloads(mock_post) + self.assertEqual(len(payloads), 3) + + def test_mixed_matching_and_non_matching(self): + """Files outside all path rules should have no SBOM, inside should have SBOM.""" + settings = self._make_settings({ + 'bom': { + 'include': [ + {'path': 'vendor/', 'purl': 'pkg:npm/vendor-lib'}, + ], + } + }) + self._create_files(['vendor/lib.c', 'src/main.c']) + scanner, mock_post = self._create_scanner(settings) + scanner.scan_folder_with_options(self.test_dir) + + payloads = self._extract_payloads(mock_post) + # 2 batches: one with SBOM (vendor/), one without (src/) + self.assertEqual(len(payloads), 2) + + has_sbom = [p for p in payloads if 'type' in p] + no_sbom = [p for p in payloads if 'type' not in p] + self.assertEqual(len(has_sbom), 1) + self.assertEqual(len(no_sbom), 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_csv_output.py b/tests/test_csv_output.py new file mode 100644 index 00000000..499e38e4 --- /dev/null +++ b/tests/test_csv_output.py @@ -0,0 +1,50 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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. +""" + +import unittest + +from scanoss.winnowing import Winnowing + + +class MyTestCase(unittest.TestCase): + """ + Exercise the Winnowing class + """ + + def test_csv_output(self): + winnowing = Winnowing(debug=True) + filename = 'test-file.c' + contents = 'c code contents' + content_types = bytes(contents, encoding='raw_unicode_escape') + wfp = winnowing.wfp_for_contents(filename, False, content_types) + print(f'WFP for {filename}: {wfp}') + self.assertIsNotNone(wfp) + filename = __file__ + wfp = winnowing.wfp_for_file(filename, filename) + print(f'WFP for {filename}: {wfp}') + self.assertIsNotNone(wfp) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_dependency_requirement.py b/tests/test_dependency_requirement.py new file mode 100644 index 00000000..571dd62d --- /dev/null +++ b/tests/test_dependency_requirement.py @@ -0,0 +1,76 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2026, SCANOSS + + 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. +""" + +import os +import unittest + +from scanoss.scancodedeps import REQUIREMENT_NAME_PREFIX_RE, ScancodeDeps + +TEST_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def _sanitize(rq): + return REQUIREMENT_NAME_PREFIX_RE.sub('', rq) + + +class TestSanitizeRequirement(unittest.TestCase): + """Test the regex that strips package names from requirements""" + + def test_pip_exact_version(self): + """pip exact match: strip name, keep '=='""" + self.assertEqual(_sanitize('gtest==1.17.0'), '==1.17.0') + + def test_pip_less_equal(self): + self.assertEqual(_sanitize('boost<=1.83.0'), '<=1.83.0') + + def test_pip_greater_equal(self): + self.assertEqual(_sanitize('requests>=2.25.1'), '>=2.25.1') + + def test_pip_range(self): + self.assertEqual(_sanitize('requests>=2.25.1,<3'), '>=2.25.1,<3') + + def test_pip_not_equal(self): + self.assertEqual(_sanitize('foo!=1.0'), '!=1.0') + + def test_npm_caret_unchanged(self): + """npm ^: no operator after name, unchanged""" + self.assertEqual(_sanitize('^4.18.0'), '^4.18.0') + + def test_npm_tilde_unchanged(self): + self.assertEqual(_sanitize('~4.18.0'), '~4.18.0') + + def test_npm_greater_equal(self): + self.assertEqual(_sanitize('>=1.0.0'), '>=1.0.0') + + def test_bare_version_unchanged(self): + """Plain version number with no operator: unchanged""" + self.assertEqual(_sanitize('1.17.0'), '1.17.0') + + def test_name_prefix_of_another_package(self): + """'requests-toolbelt>=1.0' strips full name, not partial""" + self.assertEqual(_sanitize('requests-toolbelt>=1.0'), '>=1.0') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_dependency_skip_patterns.py b/tests/test_dependency_skip_patterns.py new file mode 100644 index 00000000..7f4eb2ce --- /dev/null +++ b/tests/test_dependency_skip_patterns.py @@ -0,0 +1,169 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2026, SCANOSS + + 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. +""" + +import unittest + +from scanoss.scancodedeps import ScancodeDeps +from scanoss.scanoss_settings import ScanossSettings + + +SAMPLE_DEPS = { + 'files': [ + {'file': 'package.json', 'purls': [{'purl': 'pkg:npm/express@4.18.0'}]}, + {'file': 'vendor/package.json', 'purls': [{'purl': 'pkg:npm/lodash@4.17.21'}]}, + {'file': 'vendor/sub/package.json', 'purls': [{'purl': 'pkg:npm/axios@1.0.0'}]}, + {'file': 'third_party/lib/requirements.txt', 'purls': [{'purl': 'pkg:pypi/requests@2.28.0'}]}, + {'file': 'src/go.mod', 'purls': [{'purl': 'pkg:golang/github.com/gin-gonic/gin@1.9.0'}]}, + ] +} + + +def _make_settings(patterns): + """Helper to create a ScanossSettings with dependency skip patterns.""" + settings = ScanossSettings(debug=True) + settings.data = { + 'settings': { + 'skip': { + 'patterns': { + 'dependencies': patterns, + } + } + } + } + return settings + + +def _make_sc_deps(scanoss_settings=None): + """Helper to create a ScancodeDeps with optional settings.""" + return ScancodeDeps(debug=True, scanoss_settings=scanoss_settings) + + + +class TestDependencySkipPatterns(unittest.TestCase): + """Tests for dependency skip patterns filtering on ScancodeDeps.""" + + def test_no_settings_no_filtering(self): + """No settings -> deps returned unchanged.""" + sc = _make_sc_deps(scanoss_settings=None) + result = sc.filter_dependencies_by_path(SAMPLE_DEPS) + self.assertEqual(result, SAMPLE_DEPS) + + def test_empty_patterns_no_filtering(self): + """Empty patterns list -> deps returned unchanged.""" + settings = _make_settings([]) + sc = _make_sc_deps(scanoss_settings=settings) + result = sc.filter_dependencies_by_path(SAMPLE_DEPS) + self.assertEqual(result, SAMPLE_DEPS) + + def test_exact_path_match(self): + """Exact path match -> that file is skipped.""" + settings = _make_settings(['vendor/package.json']) + sc = _make_sc_deps(scanoss_settings=settings) + result = sc.filter_dependencies_by_path(SAMPLE_DEPS) + files = result['files'] + matched_paths = [f['file'] for f in files] + self.assertNotIn('vendor/package.json', matched_paths) + self.assertIn('package.json', matched_paths) + self.assertIn('src/go.mod', matched_paths) + + def test_glob_pattern_vendor(self): + """Glob pattern vendor/** -> all files under vendor/ skipped.""" + settings = _make_settings(['vendor/**']) + sc = _make_sc_deps(scanoss_settings=settings) + result = sc.filter_dependencies_by_path(SAMPLE_DEPS) + files = result['files'] + matched_paths = [f['file'] for f in files] + self.assertNotIn('vendor/package.json', matched_paths) + self.assertNotIn('vendor/sub/package.json', matched_paths) + self.assertIn('package.json', matched_paths) + self.assertIn('third_party/lib/requirements.txt', matched_paths) + self.assertIn('src/go.mod', matched_paths) + + def test_directory_pattern(self): + """Directory pattern third_party/ -> files under it skipped.""" + settings = _make_settings(['third_party/']) + sc = _make_sc_deps(scanoss_settings=settings) + result = sc.filter_dependencies_by_path(SAMPLE_DEPS) + files = result['files'] + matched_paths = [f['file'] for f in files] + self.assertNotIn('third_party/lib/requirements.txt', matched_paths) + self.assertIn('package.json', matched_paths) + self.assertIn('vendor/package.json', matched_paths) + + def test_multiple_patterns(self): + """Multiple patterns -> all matching files skipped.""" + settings = _make_settings(['vendor/**', 'third_party/']) + sc = _make_sc_deps(scanoss_settings=settings) + result = sc.filter_dependencies_by_path(SAMPLE_DEPS) + files = result['files'] + matched_paths = [f['file'] for f in files] + self.assertNotIn('vendor/package.json', matched_paths) + self.assertNotIn('vendor/sub/package.json', matched_paths) + self.assertNotIn('third_party/lib/requirements.txt', matched_paths) + self.assertIn('package.json', matched_paths) + self.assertIn('src/go.mod', matched_paths) + self.assertEqual(len(files), 2) + + def test_no_match_all_kept(self): + """Pattern that matches nothing -> all files kept.""" + settings = _make_settings(['nonexistent/**']) + sc = _make_sc_deps(scanoss_settings=settings) + result = sc.filter_dependencies_by_path(SAMPLE_DEPS) + self.assertEqual(len(result['files']), len(SAMPLE_DEPS['files'])) + + +class TestGetSkipPatternsDependencies(unittest.TestCase): + """Tests for ScanossSettings.get_skip_patterns('dependencies').""" + + def test_returns_correct_data(self): + """get_skip_patterns('dependencies') returns the configured patterns.""" + settings = _make_settings(['vendor/**', 'third_party/']) + result = settings.get_skip_patterns('dependencies') + self.assertEqual(result, ['vendor/**', 'third_party/']) + + def test_returns_empty_when_key_missing(self): + """get_skip_patterns('dependencies') returns [] when key is absent (backward compat).""" + settings = ScanossSettings(debug=True) + settings.data = { + 'settings': { + 'skip': { + 'patterns': { + 'scanning': ['*.log'], + } + } + } + } + result = settings.get_skip_patterns('dependencies') + self.assertEqual(result, []) + + def test_returns_empty_when_no_settings(self): + """get_skip_patterns('dependencies') returns [] with empty data.""" + settings = ScanossSettings(debug=True) + settings.data = {} + result = settings.get_skip_patterns('dependencies') + self.assertEqual(result, []) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_file_filters.py b/tests/test_file_filters.py new file mode 100644 index 00000000..eb34face --- /dev/null +++ b/tests/test_file_filters.py @@ -0,0 +1,327 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2024, SCANOSS + + 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. +""" + +import os +import shutil +import tempfile +import unittest + +from scanoss.file_filters import FileFilters +from scanoss.scanoss_settings import ScanossSettings + + +class TestFileFilters(unittest.TestCase): + def setUp(self): + self.file_filters = FileFilters(debug=True, hidden_files_folders=True, operation_type='scanning') + self.test_dir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.test_dir) + + def create_files(self, files): + for file in files: + file_path = os.path.join(self.test_dir, file) + os.makedirs(os.path.dirname(file_path), exist_ok=True) + with open(file_path, 'w') as f: + f.write('test') + + def test_default_extensions(self): + files = [ + 'file1.js', + 'file2.go', + 'file3.py', + 'file4.css', # Should be skipped by default + 'file5.doc', # Should be skipped by default + 'dir1/file6.py', + 'dir1/file7.go', + 'dir2/file8.js', + 'dir2/file9.csv', # Should be skipped by default + ] + self.create_files(files) + + expected_files = [ + 'file1.js', + 'file2.go', + 'file3.py', + 'dir1/file6.py', + 'dir1/file7.go', + 'dir2/file8.js', + ] + + filtered_files = self.file_filters.get_filtered_files_from_folder(self.test_dir) + self.assertEqual(sorted(filtered_files), sorted(expected_files)) + + def test_default_folders(self): + files = [ + '__pycache__/file1.pyc', + 'venv/file2.py', + 'dir1/nbdist/test.py', + 'dir1/eggs/test1.py', + 'dir1/file3.py', + 'dir1/file4.go', + 'dir2/wheels/test.js', + 'dir2/file5.js', + 'package.egg-info/file6.py', + ] + self.create_files(files) + + expected_files = [ + 'dir1/file3.py', + 'dir1/file4.go', + 'dir2/file5.js', + ] + + filtered_files = self.file_filters.get_filtered_files_from_folder(self.test_dir) + self.assertEqual(sorted(filtered_files), sorted(expected_files)) + + def test_default_skipped_files(self): + files = [ + 'gradlew', + 'gradlew.bat', + 'mvnw', + 'license.txt', + 'makefile', + 'normal_file.py', + 'dir1/gradle-wrapper.jar', + 'dir1/normal_file.js', + ] + self.create_files(files) + + expected_files = [ + 'normal_file.py', + 'dir1/normal_file.js', + ] + + filtered_files = self.file_filters.get_filtered_files_from_folder(self.test_dir) + self.assertEqual(sorted(filtered_files), sorted(expected_files)) + + def test_size_limits(self): + settings = ScanossSettings() + settings.data = { + 'settings': { + 'skip': { + 'sizes': { + 'scanning': [{'patterns': ['*.py'], 'min': 150, 'max': 450}], + 'fingerprinting': [{'patterns': ['*'], 'min': 150, 'max': 450}], + } + } + } + } + files = [ + 'file1.js', # 100 bytes + 'file2.py', # 200 bytes - within limits + 'file3.py', # 500 bytes - exceeds max + 'file4.py', # 100 bytes - below min + ] + + for file in files: + file_path = os.path.join(self.test_dir, file) + with open(file_path, 'w') as f: + if 'file1' in file: + f.write('a' * 100) + elif 'file2' in file: + f.write('a' * 200) + elif 'file3' in file: + f.write('a' * 500) + else: + f.write('a' * 100) + + file_filters = FileFilters( + debug=True, scanoss_settings=settings, hidden_files_folders=True, operation_type='scanning' + ) + + # For scanning, only *.py files have size limits + filtered_files = file_filters.get_filtered_files_from_folder(self.test_dir) + self.assertEqual(sorted(filtered_files), ['file1.js', 'file2.py']) + + file_filters = FileFilters( + debug=True, scanoss_settings=settings, hidden_files_folders=True, operation_type='fingerprinting' + ) + + # For fingerprinting, all files have size limits + filtered_files = file_filters.get_filtered_files_from_folder(self.test_dir) + self.assertEqual(sorted(filtered_files), ['file2.py']) + + def test_all_extensions_flag(self): + file_filters = FileFilters( + debug=True, all_extensions=True, hidden_files_folders=True, operation_type='scanning' + ) + files = [ + 'file1.js', + 'file2.css', # Would normally be skipped + 'file3.doc', # Would normally be skipped + 'dir1/file4.csv', # Would normally be skipped + ] + self.create_files(files) + + filtered_files = file_filters.get_filtered_files_from_folder(self.test_dir) + self.assertEqual(sorted(filtered_files), sorted(files)) + + def test_all_folders_flag(self): + file_filters = FileFilters(debug=True, all_folders=True, hidden_files_folders=True, operation_type='scanning') + + files = [ + '__pycache__/file1.py', # Would normally be skipped + 'venv/file2.py', # Would normally be skipped + 'dir1/nbdist/file3.py', # Would normally be skipped + 'dir1/file4.py', + ] + self.create_files(files) + + filtered_files = file_filters.get_filtered_files_from_folder(self.test_dir) + self.assertEqual(sorted(filtered_files), sorted(files)) + + def test_get_filtered_files_from_files(self): + files = [ + os.path.join(self.test_dir, 'file1.js'), + os.path.join(self.test_dir, 'file2.css'), # Should be skipped + os.path.join(self.test_dir, 'dir1/file3.py'), + os.path.join(self.test_dir, 'dir1/__pycache__/file4.py'), # Should be skipped + ] + self.create_files(files) + + filtered_files = self.file_filters.get_filtered_files_from_files(files, self.test_dir) + + expected_files = [ + 'file1.js', + 'dir1/file3.py', + ] + self.assertEqual(sorted(filtered_files), sorted(expected_files)) + + def test_hidden_files_and_folders_enabled(self): + files = [ + '.hidden_file.py', + '.hidden_dir/visible_file.py', + '.hidden_dir/.nested_hidden_file.js', + 'visible_dir/.hidden_file.go', + '.git/config', + '.hidden_dir/nested_dir/.hidden_nested_file.py', + ] + self.create_files(files) + + expected_files = [ + '.hidden_file.py', + '.hidden_dir/visible_file.py', + '.hidden_dir/.nested_hidden_file.js', + 'visible_dir/.hidden_file.go', + '.hidden_dir/nested_dir/.hidden_nested_file.py', + ] + + filtered_files = self.file_filters.get_filtered_files_from_folder(self.test_dir) + self.assertEqual(sorted(filtered_files), sorted(expected_files)) + + def test_hidden_files_and_folders_disabled(self): + file_filters = FileFilters(debug=True, hidden_files_folders=False, operation_type='scanning') + files = [ + '.hidden_file.py', + '.hidden_dir/visible_file.py', + '.hidden_dir/.nested_hidden_file.js', + 'visible_dir/.hidden_file.go', + 'visible_file.py', + '.git/config', + ] + self.create_files(files) + + expected_files = ['visible_file.py'] + + filtered_files = file_filters.get_filtered_files_from_folder(self.test_dir) + self.assertEqual(sorted(filtered_files), sorted(expected_files)) + + def test_all_extensions_mode(self): + file_filters = FileFilters(debug=True, all_extensions=True, hidden_files_folders=True) + files = [ + 'file1.css', + 'file2.doc', + 'file3.csv', + '.hidden_file.dat', + 'dir1/file4.bmp', + 'dir1/.hidden/file5.class', + 'file6.py', + ] + self.create_files(files) + + expected_files = [ + 'file1.css', + 'file2.doc', + 'file3.csv', + '.hidden_file.dat', + 'dir1/file4.bmp', + 'dir1/.hidden/file5.class', + 'file6.py', + ] + + filtered_files = file_filters.get_filtered_files_from_folder(self.test_dir) + self.assertEqual(sorted(filtered_files), sorted(expected_files)) + + def test_all_folders_mode(self): + file_filters = FileFilters(debug=True, all_folders=True, hidden_files_folders=True, operation_type='scanning') + files = [ + '__pycache__/cache.py', + 'venv/lib.py', + 'eggs/module.py', + 'wheels/util.py', + 'normal_dir/file.py', + '.git/config.py', + ] + self.create_files(files) + + expected_files = [ + '__pycache__/cache.py', + 'venv/lib.py', + 'eggs/module.py', + 'wheels/util.py', + 'normal_dir/file.py', + '.git/config.py', + ] + + filtered_files = file_filters.get_filtered_files_from_folder(self.test_dir) + self.assertEqual(sorted(filtered_files), sorted(expected_files)) + + def test_combined_all_modes(self): + file_filters = FileFilters( + debug=True, all_extensions=True, all_folders=True, hidden_files_folders=True, operation_type='scanning' + ) + files = [ + '.hidden_dir/file1.css', + '__pycache__/cache.dat', + 'venv/.hidden_file.class', + 'normal_dir/file.py', + '.config/settings.bmp', + ] + self.create_files(files) + + expected_files = [ + '.hidden_dir/file1.css', + '__pycache__/cache.dat', + 'venv/.hidden_file.class', + 'normal_dir/file.py', + '.config/settings.bmp', + ] + + filtered_files = file_filters.get_filtered_files_from_folder(self.test_dir) + self.assertEqual(sorted(filtered_files), sorted(expected_files)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_headers_filter.py b/tests/test_headers_filter.py new file mode 100644 index 00000000..dbd533ec --- /dev/null +++ b/tests/test_headers_filter.py @@ -0,0 +1,288 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" +import shutil +import tarfile +import tempfile +import unittest +from pathlib import Path + +from scanoss.header_filter import HeaderFilter + +TEST_FILES_TAR = Path(__file__).parent / 'data' / 'test_src_files.tar.gz' + + +class TestHeaderFilter(unittest.TestCase): + """ + Test suite for HeaderFilter class functionality + """ + + @classmethod + def setUpClass(cls): + """Extract test data files from tar archive.""" + cls._temp_dir = tempfile.mkdtemp() + with tarfile.open(TEST_FILES_TAR, 'r:gz') as tf: + tf.extractall(cls._temp_dir) + + @classmethod + def tearDownClass(cls): + """Clean up extracted test data.""" + shutil.rmtree(cls._temp_dir) + + def setUp(self): + """Set up test fixtures""" + self.header_filter = HeaderFilter(debug=False, quiet=True) + + def _read_test_file(self, filename: str) -> str: + """Read an extracted test file.""" + return (Path(self._temp_dir) / 'src' / filename).read_text(encoding='utf-8') + + # ------------------------------------------------------------------- + # File-based tests (mirrors scanoss.java TestHeaderFilter) + # ------------------------------------------------------------------- + + def test_java_file(self): + """Test Java file with Apache license + imports""" + contents = self._read_test_file('TokenVerifier.java') + offset = self.header_filter.filter('TokenVerifier.java', contents) + self.assertEqual(offset, 46, 'TokenVerifier.java offset should be 46') + + def test_python_file(self): + """Test Python file with MIT license docstring + imports""" + contents = self._read_test_file('results.py') + offset = self.header_filter.filter('results.py', contents) + self.assertEqual(offset, 31, 'results.py offset should be 31') + + def test_c_file(self): + """Test C file with SPDX + license block + includes""" + contents = self._read_test_file('crc32c.c') + offset = self.header_filter.filter('crc32c.c', contents) + self.assertEqual(offset, 50, 'crc32c.c offset should be 50') + + def test_typescript_file(self): + """Test TypeScript file with imports (no license header)""" + contents = self._read_test_file('FileModel.ts') + offset = self.header_filter.filter('FileModel.ts', contents) + self.assertEqual(offset, 7, 'FileModel.ts offset should be 7') + + def test_go_file(self): + """Test Go file with license + import block""" + contents = self._read_test_file('handler.go') + offset = self.header_filter.filter('handler.go', contents) + self.assertEqual(offset, 18, 'handler.go offset should be 18') + + def test_rust_file(self): + """Test Rust file with license + use statements""" + contents = self._read_test_file('config.rs') + offset = self.header_filter.filter('config.rs', contents) + self.assertEqual(offset, 21, 'config.rs offset should be 21') + + def test_kotlin_file(self): + """Test Kotlin file with Apache license + imports""" + contents = self._read_test_file('HttpClient.kt') + offset = self.header_filter.filter('HttpClient.kt', contents) + self.assertEqual(offset, 26, 'HttpClient.kt offset should be 26') + + def test_scala_file(self): + """Test Scala file with ASF license + imports""" + contents = self._read_test_file('DataFrame.scala') + offset = self.header_filter.filter('DataFrame.scala', contents) + self.assertEqual(offset, 27, 'DataFrame.scala offset should be 27') + + def test_cpp_file(self): + """Test C++ header with license + guards + includes""" + contents = self._read_test_file('StringUtils.hpp') + offset = self.header_filter.filter('StringUtils.hpp', contents) + self.assertEqual(offset, 16, 'StringUtils.hpp offset should be 16') + + def test_csharp_file(self): + """Test C# file with MIT license + usings""" + contents = self._read_test_file('ServiceProvider.cs') + offset = self.header_filter.filter('ServiceProvider.cs', contents) + self.assertEqual(offset, 12, 'ServiceProvider.cs offset should be 12') + + def test_php_file(self): + """Test PHP file — str: + path = os.path.join(self.tmp_dir, 'test.wfp') + with open(path, 'w') as f: + f.write(content) + return path + + def test_single_file_entry(self): + wfp_content = ( + 'file=abc123,100,src/main.c\n' + '4=abcdef\n' + '8=012345\n' + ) + wfp_file = self._create_wfp_file(wfp_content) + results = list(Scanner._iter_wfp_files(wfp_file)) + self.assertEqual(len(results), 1) + file_path, content = results[0] + self.assertEqual(file_path, 'src/main.c') + self.assertEqual(content, wfp_content) + + def test_multiple_file_entries(self): + wfp_content = ( + 'file=aaa,10,src/a.c\n' + '4=111111\n' + 'file=bbb,20,src/b.c\n' + '4=222222\n' + 'file=ccc,30,src/c.c\n' + '4=333333\n' + ) + wfp_file = self._create_wfp_file(wfp_content) + results = list(Scanner._iter_wfp_files(wfp_file)) + self.assertEqual(len(results), 3) + self.assertEqual(results[0][0], 'src/a.c') + self.assertEqual(results[1][0], 'src/b.c') + self.assertEqual(results[2][0], 'src/c.c') + + def test_content_preserved_exactly(self): + wfp_content = ( + 'file=aaa,10,src/a.c\n' + '4=111111\n' + '8=aaaaaa\n' + 'file=bbb,20,src/b.c\n' + '4=222222\n' + ) + wfp_file = self._create_wfp_file(wfp_content) + results = list(Scanner._iter_wfp_files(wfp_file)) + self.assertEqual(len(results), 2) + self.assertEqual(results[0][1], 'file=aaa,10,src/a.c\n4=111111\n8=aaaaaa\n') + self.assertEqual(results[1][1], 'file=bbb,20,src/b.c\n4=222222\n') + + def test_empty_file(self): + wfp_file = self._create_wfp_file('') + results = list(Scanner._iter_wfp_files(wfp_file)) + self.assertEqual(results, []) + + def test_file_entry_without_snippets(self): + wfp_content = 'file=abc123,100,src/only_header.c\n' + wfp_file = self._create_wfp_file(wfp_content) + results = list(Scanner._iter_wfp_files(wfp_file)) + self.assertEqual(len(results), 1) + file_path, content = results[0] + self.assertEqual(file_path, 'src/only_header.c') + self.assertEqual(content, 'file=abc123,100,src/only_header.c\n') + + def test_consecutive_file_entries(self): + wfp_content = ( + 'file=aaa,10,src/a.c\n' + 'file=bbb,20,src/b.c\n' + 'file=ccc,30,src/c.c\n' + ) + wfp_file = self._create_wfp_file(wfp_content) + results = list(Scanner._iter_wfp_files(wfp_file)) + self.assertEqual(len(results), 3) + self.assertEqual(results[0][0], 'src/a.c') + self.assertEqual(results[0][1], 'file=aaa,10,src/a.c\n') + self.assertEqual(results[1][0], 'src/b.c') + self.assertEqual(results[1][1], 'file=bbb,20,src/b.c\n') + self.assertEqual(results[2][0], 'src/c.c') + self.assertEqual(results[2][1], 'file=ccc,30,src/c.c\n') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_osadl.py b/tests/test_osadl.py new file mode 100644 index 00000000..6e0b929f --- /dev/null +++ b/tests/test_osadl.py @@ -0,0 +1,102 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" +import unittest + +from scanoss.osadl import Osadl + + +class TestOsadl(unittest.TestCase): + """ + Test the Osadl class + """ + + def test_initialization(self): + """Test basic initialization - data is loaded at class level""" + osadl = Osadl() + self.assertIsNotNone(osadl) + self.assertTrue(Osadl._data_loaded) + self.assertGreater(len(Osadl._shared_copyleft_data), 0) + + def test_initialization_with_debug(self): + """Test initialization with debug enabled""" + osadl = Osadl(debug=True) + self.assertTrue(osadl.debug) + + def test_is_copyleft_gpl_2_0_only(self): + """Test GPL-2.0-only is copyleft""" + osadl = Osadl() + self.assertTrue(osadl.is_copyleft('GPL-2.0-only')) + + def test_is_copyleft_gpl_2_0_or_later(self): + """Test GPL-2.0-or-later is copyleft""" + osadl = Osadl() + self.assertTrue(osadl.is_copyleft('GPL-2.0-or-later')) + + def test_is_not_copyleft_mit(self): + """Test MIT is not copyleft""" + osadl = Osadl() + self.assertFalse(osadl.is_copyleft('MIT')) + + def test_is_copyleft_case_insensitive_license_id(self): + """Test license ID lookup is case-insensitive""" + osadl = Osadl() + self.assertTrue(osadl.is_copyleft('gpl-2.0-only')) + self.assertTrue(osadl.is_copyleft('GPL-2.0-ONLY')) + self.assertTrue(osadl.is_copyleft('Gpl-2.0-Only')) + + def test_is_copyleft_unknown_license(self): + """Test unknown license returns False""" + osadl = Osadl() + self.assertFalse(osadl.is_copyleft('Unknown-License')) + + def test_is_copyleft_empty_string(self): + """Test empty string returns False""" + osadl = Osadl() + self.assertFalse(osadl.is_copyleft('')) + + def test_is_copyleft_none(self): + """Test None returns False""" + osadl = Osadl() + self.assertFalse(osadl.is_copyleft(None)) + + def test_multiple_instances_share_data(self): + """Test that multiple instances share the same class-level data""" + osadl1 = Osadl() + osadl2 = Osadl() + + # Both instances should see data loaded by first instance + result1 = osadl1.is_copyleft('GPL-2.0-only') + self.assertTrue(result1) + self.assertTrue(Osadl._data_loaded) + + # Second instance uses the same class-level shared data + result2 = osadl2.is_copyleft('MIT') + self.assertFalse(result2) + + # Verify both instances reference the same class-level data + self.assertIs(Osadl._shared_copyleft_data, Osadl._shared_copyleft_data) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_policy_inspect.py b/tests/test_policy_inspect.py new file mode 100644 index 00000000..27c6ef41 --- /dev/null +++ b/tests/test_policy_inspect.py @@ -0,0 +1,863 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2024, SCANOSS + + 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. +""" + +import json +import os +import re +import unittest +from unittest.mock import Mock, patch + +from scanoss.constants import DEFAULT_COPYLEFT_LICENSE_SOURCES, VALID_LICENSE_SOURCES +from src.scanoss.inspection.policy_check.dependency_track.project_violation import ( + DependencyTrackProjectViolationPolicyCheck, +) +from src.scanoss.inspection.policy_check.policy_check import PolicyStatus +from src.scanoss.inspection.policy_check.scanoss.copyleft import Copyleft +from src.scanoss.inspection.policy_check.scanoss.undeclared_component import UndeclaredComponent +from src.scanoss.inspection.summary.component_summary import ComponentSummary +from src.scanoss.inspection.summary.license_summary import LicenseSummary + + +class MyTestCase(unittest.TestCase): + """ + Inspect for copyleft licenses + """ + + def test_copyleft_policy(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft(filepath=input_file_name, format_type='json') + copyleft.run() + self.assertEqual(True, True) + + """ + Inspect for copyleft licenses empty path + """ + + def test_copyleft_policy_empty_path(self): + copyleft = Copyleft(filepath='', format_type='json') + success, results = copyleft.run() + self.assertTrue(success, 2) + + """ + Inspect for empty copyleft licenses + """ + + def test_empty_copyleft_policy(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result-no-copyleft.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft(filepath=input_file_name, format_type='json') + status, policy_output = copyleft.run() + details = json.loads(policy_output.details) + self.assertEqual(status, PolicyStatus.POLICY_SUCCESS.value) + self.assertEqual(details, {}) + self.assertEqual(policy_output.summary, '0 component(s) with copyleft licenses were found.\n') + + """ + Inspect for copyleft licenses include + """ + + def test_copyleft_policy_include(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft(filepath=input_file_name, format_type='json', include='MIT') + status, policy_output = copyleft.run() + has_mit_license = False + details = json.loads(policy_output.details) + for component in details['components']: + for license in component['licenses']: + if license['spdxid'] == 'MIT': + has_mit_license = True + break + + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + self.assertEqual(has_mit_license, True) + + """ + Inspect for copyleft licenses exclude + """ + + def test_copyleft_policy_exclude(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft(filepath=input_file_name, format_type='json', exclude='GPL-2.0-only') + status, policy_output = copyleft.run() + results = json.loads(policy_output.details) + self.assertEqual(results, {}) + self.assertEqual(status, PolicyStatus.POLICY_SUCCESS.value) + + """ + Inspect for copyleft licenses explicit + """ + + def test_copyleft_policy_explicit(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft(filepath=input_file_name, format_type='json', explicit='MIT') + status, policy_output = copyleft.run() + results = json.loads(policy_output.details) + self.assertEqual(len(results['components']), 2) + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + + """ + Inspect for copyleft licenses empty explicit licenses (should set the default ones) + """ + + def test_copyleft_policy_empty_explicit(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft(filepath=input_file_name, format_type='json', explicit='') + status, policy_output = copyleft.run() + results = json.loads(policy_output.details) + self.assertEqual(len(results['components']), 5) + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + + """ + Export copyleft licenses in Markdown + """ + + def test_copyleft_policy_markdown(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft(filepath=input_file_name, format_type='md', explicit='MIT') + status, policy_output = copyleft.run() + expected_detail_output = ( + '### Copyleft Licenses \n | Component | License | URL | Copyleft |\n' + ' | - | :-: | - | - |\n' + ' | pkg:npm/%40electron/rebuild | MIT | https://spdx.org/licenses/MIT.html | YES |\n' + '| pkg:npm/%40emotion/react | MIT | https://spdx.org/licenses/MIT.html | YES | \n' + ) + expected_summary_output = '2 component(s) with copyleft licenses were found.\n' + self.assertEqual( + re.sub(r'\s|\\(?!`)|\\(?=`)', '', policy_output.details), + re.sub(r'\s|\\(?!`)|\\(?=`)', '', expected_detail_output), + ) + self.assertEqual(policy_output.summary, expected_summary_output) + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + + ## Undeclared Components Policy Tests ## + + """ + Inspect for undeclared components empty path + """ + + def test_undeclared_policy_empty_path(self): + undeclared = UndeclaredComponent(filepath='', format_type='json') + success, results = undeclared.run() + self.assertTrue(success, 2) + + """ + Inspect for undeclared components + """ + + def test_undeclared_policy(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + undeclared = UndeclaredComponent(filepath=input_file_name, format_type='json', sbom_format='legacy') + status, policy_output = undeclared.run() + results = json.loads(policy_output.details) + summary = policy_output.summary + expected_summary_output = """3 undeclared component(s) were found. + Add the following snippet into your `sbom.json` file + ```json + { + "components":[ + { + "purl": "pkg:github/scanoss/jenkins-pipeline-example" + }, + { + "purl": "pkg:github/scanoss/scanner.c" + }, + { + "purl": "pkg:github/scanoss/wfp" + } + ] + }``` + """ + self.assertEqual(len(results['components']), 4) + self.assertEqual( + re.sub(r'\s|\\(?!`)|\\(?=`)', '', summary), re.sub(r'\s|\\(?!`)|\\(?=`)', '', expected_summary_output) + ) + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + + """ + Undeclared component markdown output + """ + + def test_undeclared_policy_markdown(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + undeclared = UndeclaredComponent(filepath=input_file_name, format_type='md', sbom_format='legacy') + status, policy_output = undeclared.run() + results = policy_output.details + summary = policy_output.summary + expected_details_output = """ ### Undeclared components + | Component | License | + | - | - | + | pkg:github/scanoss/jenkins-pipeline-example | unknown | + | pkg:github/scanoss/scanner.c | GPL-2.0-only | + | pkg:github/scanoss/wfp | GPL-2.0-only | """ + + expected_summary_output = """3 undeclared component(s) were found. + Add the following snippet into your `sbom.json` file + ```json + { + "components":[ + { + "purl": "pkg:github/scanoss/jenkins-pipeline-example" + }, + { + "purl": "pkg:github/scanoss/scanner.c" + }, + { + "purl": "pkg:github/scanoss/wfp" + } + ] + }``` + """ + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + self.assertEqual( + re.sub(r'\s|\\(?!`)|\\(?=`)', '', results), re.sub(r'\s|\\(?!`)|\\(?=`)', '', expected_details_output) + ) + self.assertEqual( + re.sub(r'\s|\\(?!`)|\\(?=`)', '', summary), re.sub(r'\s|\\(?!`)|\\(?=`)', '', expected_summary_output) + ) + + """ + Undeclared component markdown scanoss summary output + """ + + def test_undeclared_policy_markdown_scanoss_summary(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + undeclared = UndeclaredComponent(filepath=input_file_name, format_type='md') + status, policy_output = undeclared.run() + results = policy_output.details + summary = policy_output.summary + expected_details_output = """ ### Undeclared components + | Component | License | + | - | - | + | pkg:github/scanoss/jenkins-pipeline-example | unknown | + | pkg:github/scanoss/scanner.c | GPL-2.0-only | + | pkg:github/scanoss/wfp | GPL-2.0-only | """ + + expected_summary_output = """3 undeclared component(s) were found. + Add the following snippet into your `scanoss.json` file + + ```json + { + "bom": { + "include": [ + { + "purl": "pkg:github/scanoss/jenkins-pipeline-example" + }, + { + "purl": "pkg:github/scanoss/scanner.c" + }, + { + "purl": "pkg:github/scanoss/wfp" + } + ] + } + } + ```""" + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + self.assertEqual( + re.sub(r'\s|\\(?!`)|\\(?=`)', '', results), re.sub(r'\s|\\(?!`)|\\(?=`)', '', expected_details_output) + ) + self.assertEqual( + re.sub(r'\s|\\(?!`)|\\(?=`)', '', summary), re.sub(r'\s|\\(?!`)|\\(?=`)', '', expected_summary_output) + ) + + """ + Undeclared component sbom summary output + """ + + def test_undeclared_policy_scanoss_summary(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + undeclared = UndeclaredComponent(filepath=input_file_name) + status, policy_output = undeclared.run() + results = json.loads(policy_output.details) + summary = policy_output.summary + expected_summary_output = """3 undeclared component(s) were found. + Add the following snippet into your `scanoss.json` file + + ```json + { + "bom": { + "include": [ + { + "purl": "pkg:github/scanoss/jenkins-pipeline-example" + }, + { + "purl": "pkg:github/scanoss/scanner.c" + }, + { + "purl": "pkg:github/scanoss/wfp" + } + ] + } + } + ```""" + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + self.assertEqual(len(results['components']), 4) + self.assertEqual( + re.sub(r'\s|\\(?!`)|\\(?=`)', '', summary), re.sub(r'\s|\\(?!`)|\\(?=`)', '', expected_summary_output) + ) + + def test_undeclared_policy_jira_markdown_output(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + undeclared = UndeclaredComponent(filepath=input_file_name, format_type='jira_md') + status, policy_output = undeclared.run() + details = policy_output.details + summary = policy_output.summary + expected_details_output = """|*Component*|*License*| +|pkg:github/scanoss/jenkins-pipeline-example|unknown| +|pkg:github/scanoss/scanner.c|GPL-2.0-only| +|pkg:github/scanoss/wfp|GPL-2.0-only| +""" + expected_summary_output = """3 undeclared component(s) were found. +Add the following snippet into your `scanoss.json` file +{code:json} +{ + "bom": { + "include": [ + { + "purl": "pkg:github/scanoss/jenkins-pipeline-example" + }, + { + "purl": "pkg:github/scanoss/scanner.c" + }, + { + "purl": "pkg:github/scanoss/wfp" + } + ] + } +} +{code} +""" + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + self.assertEqual(expected_details_output, details) + self.assertEqual(summary, expected_summary_output) + + def test_copyleft_policy_jira_markdown_output(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft(filepath=input_file_name, format_type='jira_md') + status, policy_output = copyleft.run() + results = policy_output.details + expected_details_output = """### Copyleft Licenses\n|*Component*|*License*|*URL*|*Copyleft*| +|pkg:github/scanoss/scanner.c|GPL-2.0-only|https://spdx.org/licenses/GPL-2.0-only.html|YES| +|pkg:github/scanoss/engine|GPL-2.0-only|https://spdx.org/licenses/GPL-2.0-only.html|YES| +|pkg:github/scanoss/wfp|GPL-2.0-only|https://spdx.org/licenses/GPL-2.0-only.html|YES| +""" + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + self.assertEqual(expected_details_output, results) + + ## Copyleft License Source Filtering Tests ## + + def test_copyleft_policy_default_license_sources(self): + """ + Test default behavior: should use DEFAULT_COPYLEFT_LICENSE_SOURCES + (component_declared and license_file) + """ + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft(filepath=input_file_name, format_type='json') + status, policy_output = copyleft.run() + details = json.loads(policy_output.details) + + # Should find components with copyleft from component_declared or license_file + # Expected: 5 PURL@version entries (scanner.c x2, engine x2, wfp x1) + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + self.assertEqual(len(details['components']), 5) + + # Verify all components have licenses from default sources + for component in details['components']: + for license in component['licenses']: + self.assertIn(license['source'], DEFAULT_COPYLEFT_LICENSE_SOURCES) + + def test_copyleft_policy_license_sources_none(self): + """ + Test explicit None: should use DEFAULT_COPYLEFT_LICENSE_SOURCES + """ + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft(filepath=input_file_name, format_type='json', license_sources=None) + status, policy_output = copyleft.run() + details = json.loads(policy_output.details) + + # Should behave same as default + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + self.assertEqual(len(details['components']), 5) + + # Verify all components have licenses from default sources + for component in details['components']: + for license in component['licenses']: + self.assertIn(license['source'], DEFAULT_COPYLEFT_LICENSE_SOURCES) + + + def test_copyleft_policy_license_sources_component_declared_only(self): + """ + Test filtering to component_declared source only + Should find GPL-2.0-only from component_declared + """ + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft( + filepath=input_file_name, + format_type='json', + license_sources=['component_declared'] + ) + status, policy_output = copyleft.run() + details = json.loads(policy_output.details) + + # Should find 5 PURL@version entries from component_declared + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + self.assertEqual(len(details['components']), 5) + + # All licenses should be from component_declared + for component in details['components']: + for license in component['licenses']: + self.assertEqual(license['source'], 'component_declared') + + def test_copyleft_policy_license_sources_license_file_only(self): + """ + Test filtering to license_file source only + Should find GPL-2.0-only from license_file (engine and wfp) + """ + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft( + filepath=input_file_name, + format_type='json', + license_sources=['license_file'] + ) + status, policy_output = copyleft.run() + details = json.loads(policy_output.details) + + # Should find engine and wfp (2 components with license_file) + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + self.assertEqual(len(details['components']), 2) + + # Verify components are engine and wfp + purls = [comp['purl'] for comp in details['components']] + self.assertIn('pkg:github/scanoss/engine', purls) + self.assertIn('pkg:github/scanoss/wfp', purls) + + # All licenses should be from license_file + for component in details['components']: + for license in component['licenses']: + self.assertEqual(license['source'], 'license_file') + + def test_copyleft_policy_license_sources_file_header_only(self): + """ + Test filtering to file_header source only + file_header only has BSD-2-Clause and Zlib (not copyleft) + Should find no copyleft licenses + """ + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft( + filepath=input_file_name, + format_type='json', + license_sources=['file_header'] + ) + status, policy_output = copyleft.run() + details = json.loads(policy_output.details) + + # Should find no copyleft (file_header only has BSD and Zlib) + self.assertEqual(status, PolicyStatus.POLICY_SUCCESS.value) + self.assertEqual(details, {}) + + def test_copyleft_policy_license_sources_multiple_sources(self): + """ + Test using multiple license sources + Should find copyleft from component_declared and scancode + """ + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft( + filepath=input_file_name, + format_type='json', + license_sources=['component_declared', 'scancode'] + ) + status, policy_output = copyleft.run() + details = json.loads(policy_output.details) + + # Should find components from both sources + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + self.assertGreaterEqual(len(details['components']), 3) + + # Verify licenses are from specified sources + for component in details['components']: + for license in component['licenses']: + self.assertIn(license['source'], ['component_declared', 'scancode']) + + def test_copyleft_policy_license_sources_all_valid_sources(self): + """ + Test using all valid license sources + """ + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft( + filepath=input_file_name, + format_type='json', + license_sources=VALID_LICENSE_SOURCES + ) + status, policy_output = copyleft.run() + details = json.loads(policy_output.details) + + # Should find all copyleft licenses from any source + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + self.assertGreaterEqual(len(details['components']), 3) + + def test_copyleft_policy_license_sources_with_markdown_output(self): + """ + Test license source filtering works with markdown output + """ + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft( + filepath=input_file_name, + format_type='md', + license_sources=['license_file'] + ) + status, policy_output = copyleft.run() + + # Should generate markdown table + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + self.assertIn('### Copyleft Licenses', policy_output.details) + self.assertIn('Component', policy_output.details) + self.assertIn('License', policy_output.details) + self.assertIn('2 component(s) with copyleft licenses were found', policy_output.summary) + + def test_copyleft_policy_license_sources_with_include_filter(self): + """ + Test license_sources works with include filter + Filter to scancode source and include MIT (normally not copyleft) + """ + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft( + filepath=input_file_name, + format_type='json', + license_sources=['scancode'], + include='MIT' + ) + status, policy_output = copyleft.run() + details = json.loads(policy_output.details) + + # Should find MIT (added via include) and any OSADL copyleft licenses + self.assertEqual(status, PolicyStatus.POLICY_FAIL.value) + self.assertGreater(len(details.get('components', [])), 0) + + # Verify all licenses are from scancode or unknown (always included) + for component in details.get('components', []): + for license in component['licenses']: + self.assertIn(license['source'], ['scancode', 'unknown']) + + def test_copyleft_policy_license_sources_with_exclude_filter(self): + """ + Test license_sources works with exclude filter + Use component_declared but exclude GPL-2.0-only + """ + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft( + filepath=input_file_name, + format_type='json', + license_sources=['component_declared'], + exclude='GPL-2.0-only' + ) + status, policy_output = copyleft.run() + details = json.loads(policy_output.details) + + # Should exclude GPL-2.0-only, leaving nothing (all component_declared are GPL-2.0-only) + self.assertEqual(status, PolicyStatus.POLICY_SUCCESS.value) + self.assertEqual(details, {}) + + def test_copyleft_policy_license_sources_no_copyleft_file(self): + """ + Test license_sources with result-no-copyleft.json + Should return success even with license_sources specified + """ + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result-no-copyleft.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + copyleft = Copyleft( + filepath=input_file_name, + format_type='json', + license_sources=['component_declared'] + ) + status, policy_output = copyleft.run() + details = json.loads(policy_output.details) + + # Should find no copyleft + self.assertEqual(status, PolicyStatus.POLICY_SUCCESS.value) + self.assertEqual(details, {}) + self.assertIn('0 component(s) with copyleft licenses were found', policy_output.summary) + + def test_inspect_license_summary(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + i_license_summary = LicenseSummary(filepath=input_file_name) + license_summary = i_license_summary.run() + self.assertEqual(license_summary['detectedLicenses'], 3) + self.assertEqual(license_summary['detectedLicensesWithCopyleft'], 1) + + def test_inspect_license_summary_with_empty_result(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'empty-result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + i_license_summary = LicenseSummary(filepath=input_file_name) + license_summary = i_license_summary.run() + self.assertEqual(license_summary['detectedLicenses'], 0) + self.assertEqual(license_summary['detectedLicensesWithCopyleft'], 0) + self.assertEqual(len(license_summary['licenses']), 0) + + def test_inspect_component_summary(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + i_component_summary = ComponentSummary(filepath=input_file_name) + component_summary = i_component_summary.run() + print(component_summary) + self.assertEqual(component_summary['totalComponents'], 4) + self.assertEqual(component_summary['undeclaredComponents'], 3) + self.assertEqual(component_summary['declaredComponents'], 1) + self.assertEqual(component_summary['totalFilesDetected'], 10) + self.assertEqual(component_summary['totalFilesUndeclared'], 8) + self.assertEqual(component_summary['totalFilesDeclared'], 2) + + def test_inspect_component_summary_empty_result(self): + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'empty-result.json' + input_file_name = os.path.join(script_dir, 'data', file_name) + i_component_summary = ComponentSummary(filepath=input_file_name) + component_summary = i_component_summary.run() + self.assertEqual(component_summary['totalComponents'], 0) + self.assertEqual(component_summary['undeclaredComponents'], 0) + self.assertEqual(component_summary['declaredComponents'], 0) + self.assertEqual(len(component_summary['components']), 0) + self.assertEqual(component_summary['totalFilesDetected'], 0) + self.assertEqual(component_summary['totalFilesUndeclared'], 0) + self.assertEqual(component_summary['totalFilesDeclared'], 0) + + ## Dependency Track Project Violation Policy Tests ## + + @patch('src.scanoss.inspection.policy_check.dependency_track.project_violation.DependencyTrackService') + def test_dependency_track_project_violation_json_formatter(self, mock_service): + mock_service.return_value = Mock() + project_violation = DependencyTrackProjectViolationPolicyCheck( + format_type='json', + api_key='test_key', + url='http://localhost', + project_id='test_project' + ) + test_violations = [ + { + 'uuid': 'violation-1', + 'type': 'SECURITY', + 'timestamp': 1640995200000, + 'component': { + 'name': 'test-component', + 'version': '1.0.0', + 'purl': 'pkg:npm/test-component@1.0.0' + }, + 'policyCondition': { + 'policy': { + 'name': 'Security Policy', + 'violationState': 'FAIL' + } + } + } + ] + result = project_violation._json(test_violations) + self.assertEqual(result.summary, '1 policy violations were found.\n') + details = json.loads(result.details) + self.assertEqual(len(details), 1) + self.assertEqual(details[0]['type'], 'SECURITY') + + @patch('src.scanoss.inspection.policy_check.dependency_track.project_violation.DependencyTrackService') + def test_dependency_track_project_violation_markdown_formatter(self, mock_service): + mock_service.return_value = Mock() + project_violation = DependencyTrackProjectViolationPolicyCheck( + format_type='md', + api_key='test_key', + url='http://localhost', + project_id='test_project' + ) + test_violations = [ + { + 'uuid': 'violation-1', + 'type': 'SECURITY', + 'timestamp': 1640995200000, + 'component': { + 'name': 'test-component', + 'version': '1.0.0', + 'purl': 'pkg:npm/test-component@1.0.0' + }, + 'policyCondition': { + 'policy': { + 'name': 'Security Policy', + 'violationState': 'FAIL' + } + } + } + ] + result = project_violation._markdown(test_violations) + self.assertEqual(result.summary, '1 policy violations were found.\n') + self.assertIn('State', result.details) + self.assertIn('Risk Type', result.details) + self.assertIn('Policy Name', result.details) + self.assertIn('Component', result.details) + self.assertIn('Date', result.details) + + @patch('src.scanoss.inspection.policy_check.dependency_track.project_violation.DependencyTrackService') + def test_dependency_track_project_violation_sort_violations(self, mock_service): + mock_service.return_value = Mock() + project_violation = DependencyTrackProjectViolationPolicyCheck( + api_key='test_key', + url='http://localhost', + project_id='test_project' + ) + test_violations = [ + {'type': 'LICENSE', 'uuid': 'license-violation'}, + {'type': 'SECURITY', 'uuid': 'security-violation'}, + {'type': 'OTHER', 'uuid': 'other-violation'}, + {'type': 'SECURITY', 'uuid': 'security-violation-2'} + ] + sorted_violations = project_violation._sort_project_violations(test_violations) + self.assertEqual(sorted_violations[0]['type'], 'SECURITY') + self.assertEqual(sorted_violations[1]['type'], 'SECURITY') + self.assertEqual(sorted_violations[2]['type'], 'LICENSE') + self.assertEqual(sorted_violations[3]['type'], 'OTHER') + + @patch('src.scanoss.inspection.policy_check.dependency_track.project_violation.DependencyTrackService') + def test_dependency_track_project_violation_empty_violations(self, mock_service): + mock_service.return_value = Mock() + project_violation = DependencyTrackProjectViolationPolicyCheck( + format_type='json', + api_key='test_key', + url='http://localhost', + project_id='test_project' + ) + empty_violations = [] + result = project_violation._json(empty_violations) + self.assertEqual(result.summary, '0 policy violations were found.\n') + details = json.loads(result.details) + self.assertEqual(len(details), 0) + + @patch('src.scanoss.inspection.policy_check.dependency_track.project_violation.DependencyTrackService') + def test_dependency_track_project_violation_markdown_empty(self, mock_service): + mock_service.return_value = Mock() + project_violation = DependencyTrackProjectViolationPolicyCheck( + format_type='md', + api_key='test_key', + url='http://localhost', + project_id='test_project' + ) + empty_violations = [] + result = project_violation._markdown(empty_violations) + self.assertEqual(result.summary, '0 policy violations were found.\n') + self.assertIn('State', result.details) + self.assertIn('Risk Type', result.details) + + @patch('src.scanoss.inspection.policy_check.dependency_track.project_violation.DependencyTrackService') + def test_dependency_track_project_violation_multiple_types(self, mock_service): + mock_service.return_value = Mock() + project_violation = DependencyTrackProjectViolationPolicyCheck( + format_type='json', + api_key='test_key', + url='http://localhost', + project_id='test_project' + ) + test_violations = [ + { + 'uuid': 'violation-1', + 'type': 'SECURITY', + 'timestamp': 1640995200000, + 'component': { + 'name': 'vulnerable-component', + 'version': '1.0.0', + 'purl': 'pkg:npm/vulnerable-component@1.0.0' + }, + 'policyCondition': { + 'policy': { + 'name': 'Security Policy', + 'violationState': 'FAIL' + } + } + }, + { + 'uuid': 'violation-2', + 'type': 'LICENSE', + 'timestamp': 1640995300000, + 'component': { + 'name': 'license-component', + 'version': '2.0.0', + 'purl': 'pkg:npm/license-component@2.0.0' + }, + 'policyCondition': { + 'policy': { + 'name': 'License Policy', + 'violationState': 'WARN' + } + } + } + ] + result = project_violation._json(test_violations) + self.assertEqual(result.summary, '2 policy violations were found.\n') + details = json.loads(result.details) + self.assertEqual(len(details), 2) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_scan_post_processor.py b/tests/test_scan_post_processor.py new file mode 100644 index 00000000..e4598e87 --- /dev/null +++ b/tests/test_scan_post_processor.py @@ -0,0 +1,335 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2024, SCANOSS + + 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. +""" + +import json +import os +import tempfile +import unittest +from pathlib import Path + +from scanoss.scanoss_settings import ScanossSettings +from scanoss.scanpostprocessor import ScanPostProcessor + + +class MyTestCase(unittest.TestCase): + """ + Unit test cases for Scan Post-Processing + """ + + script_dir = os.path.dirname(os.path.abspath(__file__)) + scan_settings_path = Path(script_dir, 'data', 'scanoss.json').resolve() + scan_settings = ScanossSettings(filepath=scan_settings_path) + post_processor = ScanPostProcessor(scan_settings) + + result_json_path = Path(script_dir, 'data', 'result.json').resolve() + + def _load_result_data(self): + """Load result.json fixture, returning a fresh dict each time.""" + with open(self.result_json_path) as f: + return json.load(f) + + def _make_processor(self, settings_data): + """Create a ScanPostProcessor from inline settings data, returns (processor, path).""" + f = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) + json.dump(settings_data, f) + f.close() + settings = ScanossSettings(filepath=Path(f.name)) + return ScanPostProcessor(settings), f.name + + def test_remove_files(self): + """ + Should remove component if matches path and purl + """ + + results = { + 'scanoss_settings.py': [ + { + 'purl': ['pkg:github/scanoss/scanoss.py'], + } + ], + } + processed_results = self.post_processor.load_results(results).post_process() + + self.assertEqual(len(processed_results), 0) + self.assertEqual(processed_results, {}) + + def test_remove_files_no_results(self): + """ + Should return empty dictionary when empty results are provided + """ + processed_results = self.post_processor.load_results({}).post_process() + + self.assertEqual(len(processed_results), 0) + self.assertEqual(processed_results, {}) + + def test_remove_files_purl_match(self): + """ + Should remove component if matches purl + """ + results = { + 'no_matching_path.go': [ + { + 'purl': ['matching/purl'], + } + ], + } + processed_results = self.post_processor.load_results(results).post_process() + self.assertEqual(len(processed_results), 0) + self.assertEqual(processed_results, {}) + + def test_replace_purls_full_match(self): + """ + Should replace purl if full match + """ + results = { + 'full_match_test.py': [ + { + 'purl': ['pkg:github/scanoss/full_match_test.py'], + } + ], + } + processed_results = self.post_processor.load_results(results).post_process() + self.assertEqual(len(processed_results), 1) + self.assertEqual( + processed_results['full_match_test.py'][0]['purl'], ['pkg:github/scanoss/full_match_replaced.py'] + ) + + def test_replace_purls_purl_match(self): + """Should replace purl if matches purl""" + results = { + 'only_purl_match.py': [ + { + 'purl': ['pkg:github/scanoss/only_purl_match.py'], + } + ], + } + processed_results = self.post_processor.load_results(results).post_process() + self.assertEqual(len(processed_results), 1) + self.assertEqual( + processed_results['only_purl_match.py'][0]['purl'], ['pkg:github/scanoss/only_purl_match_replaced.py'] + ) + + + def test_replace_purls_with_license(self): + """Should apply the license from the replace rule to the result""" + processor, path = self._make_processor({ + 'bom': { + 'replace': [{ + 'purl': 'pkg:github/scanoss/scanner.c', + 'replace_with': 'pkg:github/scanoss/replacement', + 'license': 'Apache-2.0', + }] + } + }) + try: + processed = processor.load_results(self._load_result_data()).post_process() + + entry = processed['inc/json.h'][0] + self.assertEqual(entry['purl'], ['pkg:github/scanoss/replacement']) + self.assertEqual(entry['licenses'], [{'name': 'Apache-2.0'}]) + finally: + os.unlink(path) + + def test_replace_purls_without_license(self): + """When replace_with PURL is NOT in scan results and the rule has no + license override, licenses should be removed entirely — we have no + license info for the unknown replacement component.""" + processor, path = self._make_processor({ + 'bom': { + 'replace': [{ + 'purl': 'pkg:github/scanoss/scanner.c', + 'replace_with': 'pkg:github/scanoss/replacement', + }] + } + }) + try: + processed = processor.load_results(self._load_result_data()).post_process() + + entry = processed['inc/json.h'][0] + self.assertEqual(entry['purl'], ['pkg:github/scanoss/replacement']) + # 'replacement' is not in scan results, so there's no license info + # to copy — the original component's licenses must be reset to default + self.assertEqual(entry['licenses'], []) + finally: + os.unlink(path) + + def test_replace_with_realistic_result(self): + """Should replace a full realistic scan result and strip old metadata""" + processor, path = self._make_processor({ + 'bom': { + 'replace': [{ + 'purl': 'pkg:github/scanoss/scanner.c', + 'replace_with': 'pkg:github/scanoss/replacement', + 'license': 'GPL-2.0-only', + }] + } + }) + try: + processed = processor.load_results(self._load_result_data()).post_process() + + entry = processed['inc/json.h'][0] + self.assertEqual(entry['purl'], ['pkg:github/scanoss/replacement']) + self.assertEqual(entry['component'], 'replacement') + self.assertEqual(entry['vendor'], 'scanoss') + self.assertEqual(entry['status'], 'identified') + self.assertEqual(entry['licenses'], [{'name': 'GPL-2.0-only'}]) + # File-related fields must be preserved + self.assertIn('source_hash', entry) + self.assertIn('file', entry) + self.assertIn('file_hash', entry) + self.assertIn('file_url', entry) + # Component-level fields should be reset to defaults + self.assertEqual(entry['cryptography'], []) + self.assertEqual(entry['dependencies'], []) + self.assertEqual(entry['quality'], []) + self.assertEqual(entry['vulnerabilities'], []) + self.assertEqual(entry['health'], {}) + self.assertEqual(entry['provenance'], '') + self.assertEqual(entry['latest'], '') + self.assertEqual(entry['release_date'], '') + self.assertEqual(entry['version'], '') + self.assertEqual(entry['url_hash'], '') + self.assertEqual(entry['url_stats'], {}) + finally: + os.unlink(path) + + def test_replace_with_existing_purl_preserves_per_file_fields(self): + """When replace_with target exists in scan results (component_info_map), + per-file fields must be preserved and component-level fields copied.""" + processor, path = self._make_processor({ + 'bom': { + 'replace': [{ + 'purl': 'pkg:github/scanoss/scanner.c', + 'replace_with': 'pkg:github/scanoss/engine', + }] + } + }) + try: + processed = processor.load_results(self._load_result_data()).post_process() + + entry = processed['inc/json.h'][0] + self.assertEqual(entry['purl'], ['pkg:github/scanoss/engine']) + self.assertEqual(entry['status'], 'identified') + # Per-file fields must be from the ORIGINAL result (inc/json.h), not + # from the component_info_map entry (which came from a different file) + self.assertEqual(entry['file'], 'scanner.c-1.3.3/external/inc/json.h') + self.assertEqual(entry['file_hash'], 'e91a03b850651dd56dd979ba92668a19') + self.assertEqual(entry['source_hash'], 'e91a03b850651dd56dd979ba92668a19') + self.assertEqual(entry['lines'], 'all') + self.assertEqual(entry['matched'], '100%') + self.assertEqual(entry['oss_lines'], 'all') + # Component-level fields should come from the engine entry + self.assertEqual(entry['component'], 'engine') + self.assertEqual(entry['vendor'], 'scanoss') + # Without a license override, the component's licenses are kept + self.assertIn('licenses', entry) + self.assertTrue(len(entry['licenses']) > 0) + finally: + os.unlink(path) + + def test_replace_with_existing_purl_empty_licenses_clears_original(self): + """When replace_with PURL exists in scan results but has an empty + licenses list, the original component's licenses must be replaced + with the empty list — not left stale.""" + processor, path = self._make_processor({ + 'bom': { + 'replace': [{ + 'purl': 'pkg:github/scanoss/scanner.c', + 'replace_with': 'pkg:github/scanoss/jenkins-pipeline-example', + }] + } + }) + try: + processed = processor.load_results(self._load_result_data()).post_process() + + # inc/json.h originally had BSD-2-Clause + GPL-2.0-only licenses; + # jenkins-pipeline-example (from inc/log.c) has licenses: [] + entry = processed['inc/json.h'][0] + self.assertEqual(entry['purl'], ['pkg:github/scanoss/jenkins-pipeline-example']) + # Original licenses must NOT remain — replaced with empty list + self.assertEqual(entry['licenses'], []) + finally: + os.unlink(path) + + def test_replace_path_scoped_with_existing_purl_and_license_override(self): + """Reproduce Sean's bug: path-scoped replace rule where replace_with PURL + already exists in results from a different file. Per-file fields must be + preserved, license override must be applied, and files outside the path + must not be affected.""" + processor, path = self._make_processor({ + 'bom': { + 'replace': [{ + 'path': 'src/', + 'purl': 'pkg:github/scanoss/scanner.c', + 'replace_with': 'pkg:github/scanoss/engine', + 'license': 'GPL-3.0-only', + }] + } + }) + try: + processed = processor.load_results(self._load_result_data()).post_process() + + # src/json.c is under src/ and matches scanner.c → should be replaced + entry = processed['src/json.c'][0] + self.assertEqual(entry['purl'], ['pkg:github/scanoss/engine']) + self.assertEqual(entry['status'], 'identified') + self.assertEqual(entry['licenses'], [{'name': 'GPL-3.0-only'}]) + # Per-file fields must be from src/json.c, not from the engine entry + self.assertEqual(entry['file'], 'scanner.c-1.3.3/external/src/json.c') + self.assertEqual(entry['file_hash'], '8e4d433c1547b59681379e9fe9960546') + self.assertEqual(entry['source_hash'], '8e4d433c1547b59681379e9fe9960546') + # Component-level fields from engine + self.assertEqual(entry['component'], 'engine') + self.assertEqual(entry['vendor'], 'scanoss') + + # inc/json.h is NOT under src/ → should remain unchanged + inc_entry = processed['inc/json.h'][0] + self.assertEqual(inc_entry['purl'], ['pkg:github/scanoss/scanner.c']) + self.assertEqual(inc_entry['status'], 'pending') + finally: + os.unlink(path) + + def test_replace_purl_with_version_no_match_unversioned_result(self): + """Should NOT replace when rule has purl@version but result has no version""" + processor, path = self._make_processor({ + 'bom': { + 'replace': [{ + 'purl': 'pkg:github/scanoss/scanner.c@1.3.3', + 'replace_with': 'pkg:github/scanoss/replacement@2.0.0', + }] + } + }) + try: + processed = processor.load_results(self._load_result_data()).post_process() + + entry = processed['inc/json.h'][0] + # Should remain unchanged — result purl has no version + self.assertEqual(entry['purl'], ['pkg:github/scanoss/scanner.c']) + self.assertEqual(entry['status'], 'pending') + finally: + os.unlink(path) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_scan_settings_builder.py b/tests/test_scan_settings_builder.py new file mode 100644 index 00000000..4b73820b --- /dev/null +++ b/tests/test_scan_settings_builder.py @@ -0,0 +1,362 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" + +import os +import unittest +from pathlib import Path + +from src.scanoss.scan_settings_builder import (ScanSettingsBuilder) +from src.scanoss.scanoss_settings import ScanossSettings + + +class TestScanSettingsBuilder(unittest.TestCase): + """Tests for the ScanSettingsBuilder class.""" + + script_dir = os.path.dirname(os.path.abspath(__file__)) + scan_settings_path = Path(script_dir, 'data', 'scanoss.json').resolve() + scan_settings = ScanossSettings(filepath=scan_settings_path) + + # ========================================================================= + # Test initialization + # ========================================================================= + + def test_init_with_none_settings(self): + """Test initialization with None settings.""" + builder = ScanSettingsBuilder(None) + + self.assertIsNone(builder.scanoss_settings) + self.assertIsNone(builder.proxy) + self.assertIsNone(builder.url) + self.assertFalse(builder.ignore_cert_errors) + self.assertIsNone(builder.min_snippet_hits) + self.assertIsNone(builder.min_snippet_lines) + self.assertIsNone(builder.honour_file_exts) + self.assertIsNone(builder.ranking) + self.assertIsNone(builder.ranking_threshold) + + def test_init_with_settings(self): + """Test initialization with settings object.""" + builder = ScanSettingsBuilder(self.scan_settings) + + self.assertEqual(builder.scanoss_settings, self.scan_settings) + + # ========================================================================= + # Test static helper methods + # ========================================================================= + + def test_str_to_bool_with_none(self): + """Test _str_to_bool returns None for None input.""" + self.assertIsNone(ScanSettingsBuilder._str_to_bool(None)) + + def test_str_to_bool_with_true_string(self): + """Test _str_to_bool converts 'true' to True.""" + self.assertTrue(ScanSettingsBuilder._str_to_bool('true')) + self.assertTrue(ScanSettingsBuilder._str_to_bool('True')) + self.assertTrue(ScanSettingsBuilder._str_to_bool('TRUE')) + + def test_str_to_bool_with_false_string(self): + """Test _str_to_bool converts 'false' to False.""" + self.assertFalse(ScanSettingsBuilder._str_to_bool('false')) + self.assertFalse(ScanSettingsBuilder._str_to_bool('False')) + self.assertFalse(ScanSettingsBuilder._str_to_bool('FALSE')) + + def test_str_to_bool_with_bool_input(self): + """Test _str_to_bool passes through bool values.""" + self.assertTrue(ScanSettingsBuilder._str_to_bool(True)) + self.assertFalse(ScanSettingsBuilder._str_to_bool(False)) + + def test_merge_with_priority_file_snippet_wins(self): + """Test _merge_with_priority returns file_snippet value when present (highest priority).""" + result = ScanSettingsBuilder._merge_with_priority('cli', 'file_snippet', 'root') + self.assertEqual(result, 'file_snippet') + + def test_merge_with_priority_root_second(self): + """Test _merge_with_priority returns root when file_snippet is None.""" + result = ScanSettingsBuilder._merge_with_priority('cli', None, 'root') + self.assertEqual(result, 'root') + + def test_merge_with_priority_cli_fallback(self): + """Test _merge_with_priority returns CLI when others are None.""" + result = ScanSettingsBuilder._merge_with_priority('cli', None, None) + self.assertEqual(result, 'cli') + + def test_merge_with_priority_all_none(self): + """Test _merge_with_priority returns None when all are None.""" + result = ScanSettingsBuilder._merge_with_priority(None, None, None) + self.assertIsNone(result) + + def test_merge_cli_with_settings_settings_wins(self): + """Test _merge_cli_with_settings returns settings value when present (highest priority).""" + result = ScanSettingsBuilder._merge_cli_with_settings('cli', 'settings') + self.assertEqual(result, 'settings') + + def test_merge_cli_with_settings_cli_fallback(self): + """Test _merge_cli_with_settings returns CLI when settings is None.""" + result = ScanSettingsBuilder._merge_cli_with_settings('cli', None) + self.assertEqual(result, 'cli') + + # ========================================================================= + # Test with_proxy + # ========================================================================= + + def test_with_proxy_cli_only(self): + """Test with_proxy uses CLI value when no settings.""" + builder = ScanSettingsBuilder(None) + result = builder.with_proxy('http://cli-proxy:8080') + + self.assertEqual(builder.proxy, 'http://cli-proxy:8080') + self.assertEqual(result, builder) # Test chaining + + def test_with_proxy_from_file_snippet(self): + """Test with_proxy uses file_snippet.proxy.host when CLI is None.""" + builder = ScanSettingsBuilder(self.scan_settings) + builder.with_proxy(None) + + # file_snippet.proxy.host = "http://file-snippet-proxy:8080" + self.assertEqual(builder.proxy, 'http://file-snippet-proxy:8080') + + def test_with_proxy_settings_overrides_cli(self): + """Test with_proxy settings value overrides CLI.""" + builder = ScanSettingsBuilder(self.scan_settings) + builder.with_proxy('http://cli-proxy:8080') + + # file_snippet.proxy.host = "http://file-snippet-proxy:8080" takes priority + self.assertEqual(builder.proxy, 'http://file-snippet-proxy:8080') + + # ========================================================================= + # Test with_url + # ========================================================================= + + def test_with_url_cli_only(self): + """Test with_url uses CLI value when no settings.""" + builder = ScanSettingsBuilder(None) + builder.with_url('https://cli-api.example.com') + + self.assertEqual(builder.url, 'https://cli-api.example.com') + + def test_with_url_from_file_snippet(self): + """Test with_url uses file_snippet.http_config.base_uri.""" + builder = ScanSettingsBuilder(self.scan_settings) + builder.with_url(None) + + # file_snippet.http_config.base_uri = "https://file-snippet-api.scanoss.com" + self.assertEqual(builder.url, 'https://file-snippet-api.scanoss.com') + + def test_with_url_settings_overrides_cli(self): + """Test with_url settings value overrides CLI.""" + builder = ScanSettingsBuilder(self.scan_settings) + builder.with_url('https://cli-api.com') + + # file_snippet.http_config.base_uri = "https://file-snippet-api.scanoss.com" takes priority + self.assertEqual(builder.url, 'https://file-snippet-api.scanoss.com') + + # ========================================================================= + # Test with_ignore_cert_errors + # ========================================================================= + + def test_with_ignore_cert_errors_defaults_to_false(self): + """Test with_ignore_cert_errors defaults to False.""" + builder = ScanSettingsBuilder(None) + builder.with_ignore_cert_errors(False) + + self.assertFalse(builder.ignore_cert_errors) + + def test_with_ignore_cert_errors_cli_true(self): + """Test with_ignore_cert_errors with CLI True.""" + builder = ScanSettingsBuilder(None) + builder.with_ignore_cert_errors(True) + + self.assertTrue(builder.ignore_cert_errors) + + def test_with_ignore_cert_errors_from_file_snippet(self): + """Test with_ignore_cert_errors from file_snippet settings.""" + builder = ScanSettingsBuilder(self.scan_settings) + builder.with_ignore_cert_errors(False) + + # file_snippet.http_config.ignore_cert_errors = true + self.assertTrue(builder.ignore_cert_errors) + + def test_with_ignore_cert_errors_cli_true_overrides(self): + """Test with_ignore_cert_errors CLI True overrides settings.""" + builder = ScanSettingsBuilder(self.scan_settings) + builder.with_ignore_cert_errors(True) + + self.assertTrue(builder.ignore_cert_errors) + + # ========================================================================= + # Test with_min_snippet_hits + # ========================================================================= + + def test_with_min_snippet_hits_cli_only(self): + """Test with_min_snippet_hits uses CLI value.""" + builder = ScanSettingsBuilder(None) + builder.with_min_snippet_hits(5) + + self.assertEqual(builder.min_snippet_hits, 5) + + def test_with_min_snippet_hits_from_settings(self): + """Test with_min_snippet_hits from settings.""" + builder = ScanSettingsBuilder(self.scan_settings) + builder.with_min_snippet_hits(None) + + # file_snippet.min_snippet_hits = 10 + self.assertEqual(builder.min_snippet_hits, 10) + + def test_with_min_snippet_hits_settings_overrides_cli(self): + """Test with_min_snippet_hits settings overrides CLI.""" + builder = ScanSettingsBuilder(self.scan_settings) + builder.with_min_snippet_hits(5) + + # file_snippet.min_snippet_hits = 10 takes priority + self.assertEqual(builder.min_snippet_hits, 10) + + # ========================================================================= + # Test with_min_snippet_lines + # ========================================================================= + + def test_with_min_snippet_lines_cli_only(self): + """Test with_min_snippet_lines uses CLI value.""" + builder = ScanSettingsBuilder(None) + builder.with_min_snippet_lines(3) + + self.assertEqual(builder.min_snippet_lines, 3) + + def test_with_min_snippet_lines_from_settings(self): + """Test with_min_snippet_lines from settings.""" + builder = ScanSettingsBuilder(self.scan_settings) + builder.with_min_snippet_lines(None) + + # file_snippet.min_snippet_lines = 5 + self.assertEqual(builder.min_snippet_lines, 5) + + # ========================================================================= + # Test with_honour_file_exts + # ========================================================================= + + def test_with_honour_file_exts_cli_true(self): + """Test with_honour_file_exts with CLI 'true'.""" + builder = ScanSettingsBuilder(None) + builder.with_honour_file_exts('true') + + self.assertTrue(builder.honour_file_exts) + + def test_with_honour_file_exts_cli_false(self): + """Test with_honour_file_exts with CLI 'false'.""" + builder = ScanSettingsBuilder(None) + builder.with_honour_file_exts('false') + + self.assertFalse(builder.honour_file_exts) + + def test_with_honour_file_exts_from_settings(self): + """Test with_honour_file_exts from settings.""" + builder = ScanSettingsBuilder(self.scan_settings) + builder.with_honour_file_exts(None) + + # file_snippet.honour_file_exts = true + self.assertTrue(builder.honour_file_exts) + + def test_with_honour_file_exts_settings_overrides_cli(self): + """Test with_honour_file_exts settings overrides CLI.""" + builder = ScanSettingsBuilder(self.scan_settings) + builder.with_honour_file_exts('false') + + # file_snippet.honour_file_exts = true takes priority + self.assertTrue(builder.honour_file_exts) + + # ========================================================================= + # Test with_ranking + # ========================================================================= + + def test_with_ranking_cli_true(self): + """Test with_ranking with CLI 'true'.""" + builder = ScanSettingsBuilder(None) + builder.with_ranking('true') + + self.assertTrue(builder.ranking) + + def test_with_ranking_cli_false(self): + """Test with_ranking with CLI 'false'.""" + builder = ScanSettingsBuilder(None) + builder.with_ranking('false') + + self.assertFalse(builder.ranking) + + def test_with_ranking_from_settings(self): + """Test with_ranking from settings.""" + builder = ScanSettingsBuilder(self.scan_settings) + builder.with_ranking(None) + + # file_snippet.ranking_enabled = true + self.assertTrue(builder.ranking) + + # ========================================================================= + # Test with_ranking_threshold + # ========================================================================= + + def test_with_ranking_threshold_cli_only(self): + """Test with_ranking_threshold uses CLI value.""" + builder = ScanSettingsBuilder(None) + builder.with_ranking_threshold(50) + + self.assertEqual(builder.ranking_threshold, 10) + + def test_with_ranking_threshold_from_settings(self): + """Test with_ranking_threshold from settings.""" + builder = ScanSettingsBuilder(self.scan_settings) + builder.with_ranking_threshold(None) + + # file_snippet.ranking_threshold = 10 + self.assertEqual(builder.ranking_threshold, 10) + + # ========================================================================= + # Test method chaining + # ========================================================================= + + def test_method_chaining(self): + """Test that all with_* methods support chaining.""" + builder = ScanSettingsBuilder(None) + + result = (builder + .with_proxy('http://proxy:8080') + .with_url('https://api.example.com') + .with_ignore_cert_errors(True) + .with_min_snippet_hits(5) + .with_min_snippet_lines(3) + .with_honour_file_exts('true') + .with_ranking('true') + .with_ranking_threshold(10)) + + self.assertEqual(result, builder) + self.assertEqual(builder.proxy, 'http://proxy:8080') + self.assertEqual(builder.url, 'https://api.example.com') + self.assertTrue(builder.ignore_cert_errors) + self.assertEqual(builder.min_snippet_hits, 5) + self.assertEqual(builder.min_snippet_lines, 3) + self.assertTrue(builder.honour_file_exts) + self.assertTrue(builder.ranking) + self.assertEqual(builder.ranking_threshold, 10) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_scan_wfp_file_threaded.py b/tests/test_scan_wfp_file_threaded.py new file mode 100644 index 00000000..794a708d --- /dev/null +++ b/tests/test_scan_wfp_file_threaded.py @@ -0,0 +1,185 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2026, SCANOSS + + 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. +""" + +import os +import shutil +import tempfile +import unittest +from typing import NamedTuple, Optional +from unittest.mock import MagicMock, patch + +from scanoss.scanner import Scanner +from scanoss.scanoss_settings import SbomContext + + +class Batch(NamedTuple): + wfp: str + sbom: Optional[dict] + + +class TestScanWfpFileThreaded(unittest.TestCase): + """Tests for Scanner.scan_wfp_file_threaded() batching logic.""" + + def setUp(self): + self.tmp_dir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.tmp_dir) + + def _create_wfp_file(self, content: str) -> str: + path = os.path.join(self.tmp_dir, 'test.wfp') + with open(path, 'w') as f: + f.write(content) + return path + + def _make_scanner(self, **overrides): + """Create a Scanner with __init__ bypassed and minimal attributes set.""" + with patch.object(Scanner, '__init__', lambda self: None): + scanner = Scanner() + scanner.scanoss_settings = None + scanner.threaded_scan = MagicMock() + scanner.max_post_size = 64 * 1024 + scanner.post_file_count = 32 + scanner.nb_threads = 5 + # Patch the private __run_scan_threaded to avoid thread infrastructure + scanner._Scanner__run_scan_threaded = MagicMock(return_value=True) + for key, value in overrides.items(): + setattr(scanner, key, value) + return scanner + + def _get_batches(self, scanner): + """Return list of Batch(wfp, sbom) from queue_add calls.""" + return [ + Batch(call.args[0], call.kwargs.get('sbom')) + for call in scanner.threaded_scan.queue_add.call_args_list + ] + + # ------------------------------------------------------------------ + # Test cases + # ------------------------------------------------------------------ + + def test_single_file_queued(self): + """A single file entry produces one queue_add call with the WFP content.""" + wfp = 'file=abc123,100,src/main.c\n4=aaaabbbb\n' + wfp_file = self._create_wfp_file(wfp) + scanner = self._make_scanner() + + result = scanner.scan_wfp_file_threaded(wfp_file) + + self.assertTrue(result) + batches = self._get_batches(scanner) + self.assertEqual(len(batches), 1) + self.assertEqual(batches[0].wfp, wfp) + self.assertIsNone(batches[0].sbom) + + def test_multiple_files_single_batch(self): + """Multiple small files that fit in one batch produce a single queue_add call.""" + wfp_lines = ( + 'file=aaa,10,src/a.c\n4=11112222\n' + 'file=bbb,20,src/b.c\n4=33334444\n' + 'file=ccc,30,src/c.c\n4=55556666\n' + ) + wfp_file = self._create_wfp_file(wfp_lines) + scanner = self._make_scanner() + + scanner.scan_wfp_file_threaded(wfp_file) + + batches = self._get_batches(scanner) + self.assertEqual(len(batches), 1) + # The batch should contain all three file entries concatenated + self.assertEqual(batches[0].wfp.count('file='), 3) + + def test_file_count_flush(self): + """When post_file_count is exceeded the batch is flushed. + + The flush condition is ``wfp_file_count > post_file_count`` (checked + *after* adding the current file). With ``post_file_count=1``: + - after file a: count=1, 1>1? no + - after file b: count=2, 2>1? yes → flush [a, b] + - after file c: count=1, 1>1? no → flushed at end-of-loop [c] + """ + wfp_lines = ( + 'file=aaa,10,src/a.c\n4=11112222\n' + 'file=bbb,20,src/b.c\n4=33334444\n' + 'file=ccc,30,src/c.c\n4=55556666\n' + ) + wfp_file = self._create_wfp_file(wfp_lines) + scanner = self._make_scanner(post_file_count=1) + + scanner.scan_wfp_file_threaded(wfp_file) + + batches = self._get_batches(scanner) + self.assertEqual(len(batches), 2) + # First batch: files a and b (flushed when count reaches 2 > 1) + self.assertEqual(batches[0].wfp.count('file='), 2) + # Second batch: file c (flushed at end of loop) + self.assertEqual(batches[1].wfp.count('file='), 1) + + def test_size_limit_flush(self): + """When accumulated WFP size exceeds max_post_size the batch is flushed before adding.""" + file_a = 'file=aaa,10,src/a.c\n4=11112222\n' + file_b = 'file=bbb,20,src/b.c\n4=33334444\n' + wfp_lines = file_a + file_b + wfp_file = self._create_wfp_file(wfp_lines) + + # Set max_post_size so file_a alone fits, but file_a + file_b would not. + # The pre-add size check: (wfp_size + scan_size) >= max_post_size + size_a = len(file_a.encode('utf-8')) + size_b = len(file_b.encode('utf-8')) + scanner = self._make_scanner(max_post_size=size_a + size_b - 1) + + scanner.scan_wfp_file_threaded(wfp_file) + + batches = self._get_batches(scanner) + self.assertEqual(len(batches), 2) + # First batch: file a (flushed because adding b would exceed limit) + self.assertIn('src/a.c', batches[0].wfp) + self.assertNotIn('src/b.c', batches[0].wfp) + # Second batch: file b (flushed at end of loop) + self.assertIn('src/b.c', batches[1].wfp) + + def test_empty_wfp_file(self): + """An empty WFP file results in no queue_add calls.""" + wfp_file = self._create_wfp_file('') + scanner = self._make_scanner() + + result = scanner.scan_wfp_file_threaded(wfp_file) + + self.assertTrue(result) + scanner.threaded_scan.queue_add.assert_not_called() + + def test_returns_true_on_success(self): + """Method returns True when __run_scan_threaded returns True.""" + wfp = 'file=abc123,100,src/main.c\n4=aaaabbbb\n' + wfp_file = self._create_wfp_file(wfp) + scanner = self._make_scanner() + + result = scanner.scan_wfp_file_threaded(wfp_file) + + self.assertTrue(result) + scanner._Scanner__run_scan_threaded.assert_called_once() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_scanner_hfh.py b/tests/test_scanner_hfh.py new file mode 100644 index 00000000..4f2bbf8a --- /dev/null +++ b/tests/test_scanner_hfh.py @@ -0,0 +1,358 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2026, SCANOSS + + 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. +""" + +import hashlib +import os +import tempfile +import unittest +from pathlib import Path +from unittest.mock import MagicMock, patch + +from scanoss.scanners.scanner_hfh import ScannerHFHPresenter + + +class TestExtractBestComponents(unittest.TestCase): + """Tests for ScannerHFHPresenter._extract_best_components""" + + def test_single_result_with_best_component(self): + hfh_results = [ + { + 'path_id': 'src/lib', + 'components': [ + { + 'order': 1, + 'name': 'best-comp', + 'versions': [{'version': '1.0.0', 'score': 95}], + }, + { + 'order': 2, + 'name': 'other-comp', + 'versions': [{'version': '2.0.0', 'score': 50}], + }, + ], + } + ] + result = ScannerHFHPresenter._extract_best_components(hfh_results) + self.assertIn('src/lib', result) + component, version = result['src/lib'] + self.assertEqual(component['name'], 'best-comp') + self.assertEqual(version['version'], '1.0.0') + + def test_no_order_1_component_skipped(self): + hfh_results = [ + { + 'path_id': 'src/lib', + 'components': [ + {'order': 2, 'name': 'comp', 'versions': [{'version': '1.0.0'}]}, + ], + } + ] + result = ScannerHFHPresenter._extract_best_components(hfh_results) + self.assertEqual(result, {}) + + def test_empty_components_skipped(self): + hfh_results = [{'path_id': 'src/lib', 'components': []}] + result = ScannerHFHPresenter._extract_best_components(hfh_results) + self.assertEqual(result, {}) + + def test_component_without_versions_skipped(self): + hfh_results = [ + { + 'path_id': 'src/lib', + 'components': [{'order': 1, 'name': 'comp', 'versions': []}], + } + ] + result = ScannerHFHPresenter._extract_best_components(hfh_results) + self.assertEqual(result, {}) + + def test_default_path_id_is_dot(self): + hfh_results = [ + { + 'components': [ + {'order': 1, 'name': 'comp', 'versions': [{'version': '1.0'}]}, + ], + } + ] + result = ScannerHFHPresenter._extract_best_components(hfh_results) + self.assertIn('.', result) + + def test_multiple_results(self): + hfh_results = [ + { + 'path_id': 'a', + 'components': [ + {'order': 1, 'name': 'comp-a', 'versions': [{'version': '1.0'}]}, + ], + }, + { + 'path_id': 'b', + 'components': [ + {'order': 1, 'name': 'comp-b', 'versions': [{'version': '2.0'}]}, + ], + }, + ] + result = ScannerHFHPresenter._extract_best_components(hfh_results) + self.assertEqual(len(result), 2) + self.assertEqual(result['a'][0]['name'], 'comp-a') + self.assertEqual(result['b'][0]['name'], 'comp-b') + + def test_empty_results_list(self): + result = ScannerHFHPresenter._extract_best_components([]) + self.assertEqual(result, {}) + + def test_first_version_is_selected(self): + hfh_results = [ + { + 'path_id': '.', + 'components': [ + { + 'order': 1, + 'name': 'comp', + 'versions': [ + {'version': '3.0', 'score': 100}, + {'version': '2.0', 'score': 80}, + ], + }, + ], + } + ] + result = ScannerHFHPresenter._extract_best_components(hfh_results) + _, version = result['.'] + self.assertEqual(version['version'], '3.0') + + +class TestFileMatchesPathId(unittest.TestCase): + """Tests for ScannerHFHPresenter._file_matches_path_id""" + + def test_root_path_matches_all(self): + self.assertTrue(ScannerHFHPresenter._file_matches_path_id('any/file.py', '.')) + + def test_exact_match(self): + self.assertTrue(ScannerHFHPresenter._file_matches_path_id('src/lib', 'src/lib')) + + def test_file_under_path_id(self): + self.assertTrue( + ScannerHFHPresenter._file_matches_path_id(f'src/lib{os.sep}file.py', 'src/lib') + ) + + def test_file_not_under_path_id(self): + self.assertFalse(ScannerHFHPresenter._file_matches_path_id('other/file.py', 'src/lib')) + + def test_partial_prefix_no_match(self): + # 'src/library' should NOT match path_id 'src/lib' + self.assertFalse(ScannerHFHPresenter._file_matches_path_id('src/library/file.py', 'src/lib')) + + def test_empty_file_path(self): + self.assertFalse(ScannerHFHPresenter._file_matches_path_id('', 'src/lib')) + + def test_root_path_matches_nested(self): + self.assertTrue(ScannerHFHPresenter._file_matches_path_id('a/b/c/d.py', '.')) + + +class TestComputeFileMd5(unittest.TestCase): + """Tests for ScannerHFHPresenter._compute_file_md5""" + + def _make_presenter(self): + mock_scanner = MagicMock() + return ScannerHFHPresenter(mock_scanner) + + def test_correct_md5(self): + presenter = self._make_presenter() + with tempfile.NamedTemporaryFile(delete=False) as f: + f.write(b'hello world') + f.flush() + path = Path(f.name) + try: + expected = hashlib.md5(b'hello world').hexdigest() + self.assertEqual(presenter._compute_file_md5(path), expected) + finally: + os.unlink(path) + + def test_empty_file(self): + presenter = self._make_presenter() + with tempfile.NamedTemporaryFile(delete=False) as f: + path = Path(f.name) + try: + expected = hashlib.md5(b'').hexdigest() + self.assertEqual(presenter._compute_file_md5(path), expected) + finally: + os.unlink(path) + + def test_nonexistent_file_returns_empty(self): + presenter = self._make_presenter() + path = Path('/nonexistent/file/that/does/not/exist.txt') + self.assertEqual(presenter._compute_file_md5(path), '') + + +class TestBuildFileMatchEntry(unittest.TestCase): + """Tests for ScannerHFHPresenter._build_file_match_entry""" + + @patch('scanoss.scanners.scanner_hfh.purl2url') + def test_basic_entry(self, mock_purl2url): + mock_purl2url.get_repo_url.return_value = 'https://github.com/vendor/comp' + component = {'purl': 'pkg:github/vendor/comp', 'name': 'comp', 'vendor': 'vendor'} + # HFH API license format + best_version = { + 'version': '1.0.0', + 'licenses': [ + {'name': 'MIT License', 'spdx_id': 'MIT', 'is_spdx_approved': True, 'url': 'https://spdx.org/licenses/MIT.html'}, + ], + } + + entry = ScannerHFHPresenter._build_file_match_entry( + component, best_version, 'src/file.py', 'abc123', 'https://api.example.com' + ) + + self.assertEqual(entry['id'], 'file') + self.assertEqual(entry['matched'], '100%') + self.assertEqual(entry['purl'], ['pkg:github/vendor/comp']) + self.assertEqual(entry['component'], 'comp') + self.assertEqual(entry['vendor'], 'vendor') + self.assertEqual(entry['version'], '1.0.0') + self.assertEqual(entry['latest'], '1.0.0') + self.assertEqual(entry['url'], 'https://github.com/vendor/comp') + self.assertEqual(entry['file'], 'src/file.py') + self.assertEqual(entry['file_hash'], 'abc123') + self.assertEqual(entry['file_url'], 'https://api.example.com/file_contents/abc123') + self.assertEqual(entry['source_hash'], 'abc123') + self.assertEqual(entry['url_hash'], '') + self.assertEqual(entry['release_date'], '') + # License should be transformed from HFH format to snippet-scanner format + self.assertEqual(len(entry['licenses']), 1) + lic = entry['licenses'][0] + self.assertEqual(lic['name'], 'MIT') + self.assertEqual(lic['source'], 'component_declared') + self.assertEqual(lic['url'], 'https://spdx.org/licenses/MIT.html') + self.assertEqual(lic['patent_hints'], '') + self.assertEqual(lic['copyleft'], '') + self.assertEqual(lic['checklist_url'], '') + self.assertEqual(lic['incompatible_with'], '') + self.assertEqual(lic['osadl_updated'], '') + self.assertEqual(entry['lines'], 'all') + self.assertEqual(entry['oss_lines'], 'all') + self.assertEqual(entry['status'], 'pending') + + @patch('scanoss.scanners.scanner_hfh.purl2url') + def test_empty_purl(self, mock_purl2url): + component = {'purl': '', 'name': 'comp', 'vendor': 'vendor'} + best_version = {'version': '1.0.0', 'licenses': []} + + entry = ScannerHFHPresenter._build_file_match_entry( + component, best_version, 'file.py', 'hash', 'https://api.example.com' + ) + + self.assertEqual(entry['purl'], ['']) + self.assertEqual(entry['url'], '') + mock_purl2url.get_repo_url.assert_not_called() + + @patch('scanoss.scanners.scanner_hfh.purl2url') + def test_missing_fields_use_defaults(self, mock_purl2url): + mock_purl2url.get_repo_url.return_value = '' + component = {} + best_version = {} + + entry = ScannerHFHPresenter._build_file_match_entry( + component, best_version, 'file.py', 'hash', 'https://api.example.com' + ) + + self.assertEqual(entry['purl'], ['']) + self.assertEqual(entry['component'], '') + self.assertEqual(entry['vendor'], '') + self.assertEqual(entry['version'], '') + self.assertEqual(entry['licenses'], []) + + @patch('scanoss.scanners.scanner_hfh.purl2url') + def test_license_uses_spdx_id_as_name(self, mock_purl2url): + mock_purl2url.get_repo_url.return_value = '' + component = {'purl': 'pkg:github/v/c', 'name': 'c', 'vendor': 'v'} + best_version = { + 'version': '1.0', + 'licenses': [ + {'name': 'GNU General Public License v2.0 only', 'spdx_id': 'GPL-2.0-only', 'is_spdx_approved': True, 'url': 'https://spdx.org/licenses/GPL-2.0-only.html'}, + ], + } + + entry = ScannerHFHPresenter._build_file_match_entry( + component, best_version, 'file.py', 'hash', 'https://api.example.com' + ) + + lic = entry['licenses'][0] + self.assertEqual(lic['name'], 'GPL-2.0-only') + self.assertEqual(lic['url'], 'https://spdx.org/licenses/GPL-2.0-only.html') + + @patch('scanoss.scanners.scanner_hfh.purl2url') + def test_license_without_spdx_id_falls_back_to_name(self, mock_purl2url): + mock_purl2url.get_repo_url.return_value = '' + component = {'purl': 'pkg:github/v/c', 'name': 'c', 'vendor': 'v'} + best_version = { + 'version': '1.0', + 'licenses': [{'name': 'Some Custom License'}], + } + + entry = ScannerHFHPresenter._build_file_match_entry( + component, best_version, 'file.py', 'hash', 'https://api.example.com' + ) + + lic = entry['licenses'][0] + self.assertEqual(lic['name'], 'Some Custom License') + self.assertEqual(lic['url'], '') + + @patch('scanoss.scanners.scanner_hfh.purl2url') + def test_multiple_licenses_transformed(self, mock_purl2url): + mock_purl2url.get_repo_url.return_value = '' + component = {'purl': 'pkg:github/v/c', 'name': 'c', 'vendor': 'v'} + best_version = { + 'version': '1.0', + 'licenses': [ + {'name': 'MIT License', 'spdx_id': 'MIT', 'is_spdx_approved': True, 'url': 'https://spdx.org/licenses/MIT.html'}, + {'name': 'Apache License 2.0', 'spdx_id': 'Apache-2.0', 'is_spdx_approved': True, 'url': 'https://spdx.org/licenses/Apache-2.0.html'}, + ], + } + + entry = ScannerHFHPresenter._build_file_match_entry( + component, best_version, 'file.py', 'hash', 'https://api.example.com' + ) + + self.assertEqual(len(entry['licenses']), 2) + self.assertEqual(entry['licenses'][0]['name'], 'MIT') + self.assertEqual(entry['licenses'][0]['url'], 'https://spdx.org/licenses/MIT.html') + self.assertEqual(entry['licenses'][1]['name'], 'Apache-2.0') + self.assertEqual(entry['licenses'][1]['url'], 'https://spdx.org/licenses/Apache-2.0.html') + + @patch('scanoss.scanners.scanner_hfh.purl2url') + def test_purl2url_returns_none(self, mock_purl2url): + mock_purl2url.get_repo_url.return_value = None + component = {'purl': 'pkg:github/vendor/comp', 'name': 'comp', 'vendor': 'vendor'} + best_version = {'version': '1.0.0', 'licenses': []} + + entry = ScannerHFHPresenter._build_file_match_entry( + component, best_version, 'file.py', 'hash', 'https://api.example.com' + ) + + # url should fallback to '' when purl2url returns None/falsy + self.assertEqual(entry['url'], '') + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_spdxlite.py b/tests/test_spdxlite.py new file mode 100644 index 00000000..7683a4da --- /dev/null +++ b/tests/test_spdxlite.py @@ -0,0 +1,231 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2025, SCANOSS + + 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. +""" +import json +import os +import tempfile +import unittest + +from scanoss.spdxlite import SpdxLite + + +class MyTestCase(unittest.TestCase): + """ + Exercise the SpdxLite class + """ + def testSpdxLite(self): + temp_dir = tempfile.gettempdir() + spdx_lite_output = os.path.join(temp_dir, "spdxlite.json") + test_data_dir = os.path.dirname(os.path.abspath(__file__)) + file_name = 'result.json' + input_file_name = os.path.join(test_data_dir, 'data', file_name) + spdx_lite = SpdxLite(debug = False, output_file=spdx_lite_output) + spdx_lite.produce_from_file(input_file_name) + md5_length = 32 + # Read data using absolute path + with open(spdx_lite_output, 'r') as f: + parsed_data = json.load(f) + spdx_version = parsed_data.get("spdxVersion") + spdx_id = parsed_data.get("SPDXID") + name = parsed_data.get("name") + organization = parsed_data.get("creationInfo",{}).get('creators')[2] + creation_info_comment = parsed_data.get("creationInfo", {}).get('comment') + document_describes = parsed_data.get("documentDescribes") + packages = parsed_data.get("packages") + + self.assertEqual(spdx_version, "SPDX-2.2") + self.assertEqual(spdx_id, "SPDXRef-DOCUMENT") + self.assertEqual(name, "SCANOSS-SBOM") + self.assertEqual(organization, "Organization: SCANOSS") + self.assertEqual(creation_info_comment, "SBOM Build information - SBOM Type: Build") + self.assertEqual(len(document_describes), 6) + self.assertEqual(len(packages), 6) + + for package in packages: + for checksum in package.get("checksums", []): + self.assertEqual(checksum.get("algorithm"), "MD5") #Check all algorithms be MD5 + self.assertEqual(len(checksum.get("checksumValue")), md5_length) #Check checksum length value be 32 + + + os.remove(spdx_lite_output) #Removes tmp spdxlite.json file + + +class SpdxLiteCpeTests(unittest.TestCase): + """ + Exercise CPE extraction and SPDX externalRefs emission. + """ + + @staticmethod + def _build_raw(vulnerabilities, purl='pkg:github/postgres/postgres'): + return { + 'src/main.c': [{ + 'id': 'file', + 'component': 'postgresql', + 'vendor': 'postgresql', + 'version': '17.0', + 'latest': '17.0', + 'url': 'https://www.postgresql.org', + 'url_hash': 'abc123', + 'download_url': 'https://example.com/pg.tar.gz', + 'purl': [purl], + 'licenses': [{'name': 'PostgreSQL', 'source': 'component_declared'}], + 'vulnerabilities': vulnerabilities, + }] + } + + def _run(self, raw): + fd, out_path = tempfile.mkstemp(prefix='spdxlite_cpe_', suffix='.json') + os.close(fd) # SpdxLite re-opens the path itself for writing + try: + spdx = SpdxLite(debug=False, output_file=out_path) + spdx.produce_from_json(raw) + with open(out_path, 'r') as f: + return json.load(f) + finally: + if os.path.exists(out_path): + os.remove(out_path) + + def _security_refs(self, doc): + refs = doc['packages'][0]['externalRefs'] + return [r for r in refs if r['referenceCategory'] == 'SECURITY'] + + def test_cpe23_emits_cpe23Type(self): + cpe = 'cpe:2.3:a:postgresql:postgresql:17.0:*:*:*:*:*:*:*' + doc = self._run(self._build_raw([{'ID': cpe, 'source': 'nvd'}])) + refs = self._security_refs(doc) + self.assertEqual(len(refs), 1) + self.assertEqual(refs[0]['referenceType'], 'cpe23Type') + self.assertEqual(refs[0]['referenceLocator'], cpe) + + def test_legacy_cpe22_slash_emits_cpe22Type(self): + cpe = 'cpe:/a:postgresql:postgresql:17.0' + doc = self._run(self._build_raw([{'ID': cpe, 'source': 'nvd'}])) + refs = self._security_refs(doc) + self.assertEqual(len(refs), 1) + self.assertEqual(refs[0]['referenceType'], 'cpe22Type') + self.assertEqual(refs[0]['referenceLocator'], cpe) + + def test_explicit_cpe22_prefix_emits_cpe22Type(self): + cpe = 'cpe:2.2:a:postgresql:postgresql:17.0' + doc = self._run(self._build_raw([{'ID': cpe, 'source': 'nvd'}])) + refs = self._security_refs(doc) + self.assertEqual(len(refs), 1) + self.assertEqual(refs[0]['referenceType'], 'cpe22Type') + + def test_case_insensitive_prefix_detection(self): + cpe = 'CPE:2.3:a:postgresql:postgresql:17.0:*:*:*:*:*:*:*' + doc = self._run(self._build_raw([{'ID': cpe, 'source': 'nvd'}])) + refs = self._security_refs(doc) + self.assertEqual(len(refs), 1) + self.assertEqual(refs[0]['referenceType'], 'cpe23Type') + self.assertEqual(refs[0]['referenceLocator'], cpe) # casing preserved in locator + + def test_duplicate_cpes_are_deduplicated(self): + cpe = 'cpe:2.3:a:postgresql:postgresql:17.0:*:*:*:*:*:*:*' + doc = self._run(self._build_raw([ + {'ID': cpe, 'source': 'nvd'}, + {'ID': cpe, 'source': 'nvd'}, + {'ID': cpe, 'source': 'nvd'}, + ])) + refs = self._security_refs(doc) + self.assertEqual(len(refs), 1) + + def test_dedup_is_case_insensitive_and_preserves_first_locator(self): + lower = 'cpe:2.3:a:postgresql:postgresql:17.0:*:*:*:*:*:*:*' + upper = 'CPE:2.3:A:POSTGRESQL:POSTGRESQL:17.0:*:*:*:*:*:*:*' + doc = self._run(self._build_raw([ + {'ID': lower, 'source': 'nvd'}, + {'ID': upper, 'source': 'nvd'}, + ])) + refs = self._security_refs(doc) + self.assertEqual(len(refs), 1) + self.assertEqual(refs[0]['referenceLocator'], lower) # first-seen wins + + def test_cve_entries_are_ignored(self): + doc = self._run(self._build_raw([ + {'ID': 'CVE-2024-12345', 'CVE': 'CVE-2024-12345', + 'source': 'nvd', 'severity': 'high'}, + {'ID': 'GHSA-xxxx-yyyy-zzzz', 'source': 'github'}, + ])) + refs = self._security_refs(doc) + self.assertEqual(refs, []) + + def test_mixed_cpe_versions_in_same_component(self): + cpe23 = 'cpe:2.3:a:postgresql:postgresql:17.0:*:*:*:*:*:*:*' + cpe22 = 'cpe:/a:postgresql:postgresql:17.0' + doc = self._run(self._build_raw([ + {'ID': cpe23, 'source': 'nvd'}, + {'ID': cpe22, 'source': 'nvd'}, + ])) + refs = self._security_refs(doc) + self.assertEqual(len(refs), 2) + types = {r['referenceType']: r['referenceLocator'] for r in refs} + self.assertEqual(types['cpe23Type'], cpe23) + self.assertEqual(types['cpe22Type'], cpe22) + + def test_unknown_cpe_format_falls_back_to_cpe23Type(self): + odd_cpe = 'cpe:weird-format:postgresql:17.0' + doc = self._run(self._build_raw([{'ID': odd_cpe, 'source': 'nvd'}])) + refs = self._security_refs(doc) + self.assertEqual(len(refs), 1) + self.assertEqual(refs[0]['referenceType'], 'cpe23Type') + self.assertEqual(refs[0]['referenceLocator'], odd_cpe) + + def test_no_vulnerabilities_field_produces_no_security_refs(self): + raw = self._build_raw([]) + # Drop the key entirely to simulate entries without a vulnerabilities block + del raw['src/main.c'][0]['vulnerabilities'] + doc = self._run(raw) + self.assertEqual(self._security_refs(doc), []) + # PURL externalRef must still be present + refs = doc['packages'][0]['externalRefs'] + self.assertEqual(len(refs), 1) + self.assertEqual(refs[0]['referenceType'], 'purl') + + def test_empty_vulnerabilities_list_produces_no_security_refs(self): + doc = self._run(self._build_raw([])) + self.assertEqual(self._security_refs(doc), []) + + def test_dependency_entries_do_not_emit_cpes(self): + raw = { + 'package.json': [{ + 'id': 'dependency', + 'dependencies': [{ + 'purl': 'pkg:npm/left-pad', + 'component': 'left-pad', + 'version': '1.3.0', + 'url': 'https://npmjs.com/package/left-pad', + 'licenses': [{'name': 'MIT', 'source': 'component_declared'}], + }] + }] + } + doc = self._run(raw) + self.assertEqual(self._security_refs(doc), []) + + def test_lowercase_id_key_is_also_supported(self): + cpe = 'cpe:2.3:a:postgresql:postgresql:17.0:*:*:*:*:*:*:*' + # Raw scan output has been known to use 'id' (lowercase) occasionally + doc = self._run(self._build_raw([{'id': cpe, 'source': 'nvd'}])) + refs = self._security_refs(doc) + self.assertEqual(len(refs), 1) + self.assertEqual(refs[0]['referenceType'], 'cpe23Type') \ No newline at end of file diff --git a/tests/test_winnowing.py b/tests/test_winnowing.py new file mode 100644 index 00000000..a5b42486 --- /dev/null +++ b/tests/test_winnowing.py @@ -0,0 +1,535 @@ +""" +SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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. +""" + +import platform +import unittest +from unittest.mock import patch + +from scanoss.winnowing import Winnowing + + +class MyTestCase(unittest.TestCase): + """ + Exercise the Winnowing class + """ + + def test_winnowing(self): + winnowing = Winnowing(debug=True) + filename = 'test-file.c' + contents = 'c code contents' + content_types = bytes(contents, encoding='raw_unicode_escape') + wfp = winnowing.wfp_for_contents(filename, False, content_types) + print(f'WFP for {filename}: {wfp}') + self.assertIsNotNone(wfp) + filename = __file__ + wfp = winnowing.wfp_for_file(filename, filename) + print(f'WFP for {filename}: {wfp}') + self.assertIsNotNone(wfp) + + def test_snippet_skip(self): + winnowing = Winnowing(debug=True) + filename = 'test-file.jar' + contents = 'jar file contents' + content_types = bytes(contents, encoding='raw_unicode_escape') + wfp = winnowing.wfp_for_contents(filename, False, content_types) + print(f'WFP for {filename}: {wfp}') + self.assertIsNotNone(wfp) + + def test_snippet_strip(self): + winnowing = Winnowing( + debug=True, hpsm=True, strip_snippet_ids=['d5e54c33,b03faabe'], strip_hpsm_ids=['0d2fffaffc62d18'] + ) + filename = 'test-file.py' + with open(__file__, 'rb') as f: + contents = f.read() + print('--- Test snippet and HPSM strip ---') + wfp = winnowing.wfp_for_contents(filename, False, contents) + found = 0 + print(f'WFP for {filename}: {wfp}') + try: + found = wfp.index('d5e54c33,b03faabe') + except ValueError: + found = -1 + self.assertEqual(found, -1) + + try: + found = wfp.index('0d2fffaffc62d18') + except ValueError: + found = -1 + self.assertEqual(found, -1) + + def test_windows_hash_calculation(self): + """Test Windows-specific hash calculation with CRLF line endings.""" + import hashlib + + # Test content with LF line endings + content_lf = b'line1\nline2\nline3\n' + # Expected content with CRLF line endings for Windows hash + content_crlf = b'line1\r\nline2\r\nline3\r\n' + + # Calculate the expected Windows hash manually + expected_windows_hash = hashlib.md5(content_crlf).hexdigest() + lf_hash = hashlib.md5(content_lf).hexdigest() + + print(f'LF content hash: {lf_hash}') + print(f'CRLF content hash (Windows): {expected_windows_hash}') + + # They should be different + self.assertNotEqual(lf_hash, expected_windows_hash) + + @patch('platform.system') + def test_windows_wfp_includes_fh2(self, mock_platform): + """Test that WFP includes fh2 hash when running on Windows.""" + # Mock Windows environment + mock_platform.return_value = 'Windows' + winnowing = Winnowing(debug=True) + + filename = 'test-file.c' + content = b'int main() {\n return 0;\n}\n' + + wfp = winnowing.wfp_for_contents(filename, False, content) + + print(f'Windows WFP output:\n{wfp}') + + # Check that WFP contains fh2 line + self.assertIn('fh2=', wfp) + + # Extract the fh2 hash from WFP + lines = wfp.split('\n') + fh2_line = [line for line in lines if line.startswith('fh2=')] + self.assertEqual(len(fh2_line), 1) + + fh2_hash = fh2_line[0].split('=')[1] + + # Verify it matches expected CRLF conversion + import hashlib + content_crlf = content.replace(b'\n', b'\r\n') + expected_hash = hashlib.md5(content_crlf).hexdigest() + self.assertEqual(fh2_hash, expected_hash) + + def test_line_ending_detection(self): + """Test line ending detection logic.""" + winnowing = Winnowing(debug=True) + + # Test LF only + content_lf = b'line1\nline2\nline3\n' + has_crlf, has_lf, has_cr = winnowing._Winnowing__detect_line_endings(content_lf) + self.assertFalse(has_crlf) + self.assertTrue(has_lf) + self.assertFalse(has_cr) + + # Test CRLF only + content_crlf = b'line1\r\nline2\r\nline3\r\n' + has_crlf, has_lf, has_cr = winnowing._Winnowing__detect_line_endings(content_crlf) + self.assertTrue(has_crlf) + self.assertFalse(has_lf) + self.assertFalse(has_cr) + + # Test CR only (old Mac style) + content_cr = b'line1\rline2\rline3\r' + has_crlf, has_lf, has_cr = winnowing._Winnowing__detect_line_endings(content_cr) + self.assertFalse(has_crlf) + self.assertFalse(has_lf) + self.assertTrue(has_cr) + + # Test mixed CRLF and LF + content_mixed = b'line1\r\nline2\nline3\r\n' + has_crlf, has_lf, has_cr = winnowing._Winnowing__detect_line_endings(content_mixed) + self.assertTrue(has_crlf) + self.assertTrue(has_lf) + self.assertFalse(has_cr) + + def test_opposite_hash_logic(self): + """Test the logic of opposite hash calculation.""" + winnowing = Winnowing(debug=True) + + # Test different line ending scenarios + content_lf = b'line1\nline2\nline3\n' + content_crlf = b'line1\r\nline2\r\nline3\r\n' + content_cr = b'line1\rline2\rline3\r' + content_mixed = b'line1\r\nline2\nline3\r' + + hash_lf = winnowing._Winnowing__calculate_opposite_line_ending_hash(content_lf) + hash_crlf = winnowing._Winnowing__calculate_opposite_line_ending_hash(content_crlf) + hash_cr = winnowing._Winnowing__calculate_opposite_line_ending_hash(content_cr) + hash_mixed = winnowing._Winnowing__calculate_opposite_line_ending_hash(content_mixed) + + print(f'LF opposite hash: {hash_lf}') + print(f'CRLF opposite hash: {hash_crlf}') + print(f'CR opposite hash: {hash_cr}') + print(f'Mixed opposite hash: {hash_mixed}') + + # LF, CR, and mixed content should all produce CRLF hash (same result) + self.assertEqual(hash_lf, hash_cr) + self.assertEqual(hash_lf, hash_mixed) + + # CRLF content should produce LF hash (different from the others) + self.assertNotEqual(hash_crlf, hash_lf) + + @unittest.skipUnless(platform.system() == 'Windows', 'Windows-specific test') + def test_actual_windows_behavior(self): + """Test actual Windows behavior when running on Windows.""" + winnowing = Winnowing(debug=True) + filename = 'test-file.c' + content = b'int main() {\n return 0;\n}\n' + + wfp = winnowing.wfp_for_contents(filename, False, content) + + print(f'Actual Windows WFP:\n{wfp}') + + # On actual Windows with LF content, should include fh2 + # Should always generate fh2 when line endings are present + self.assertIn('fh2=', wfp) + + def test_empty_file_fh2(self): + """Test fh2 behavior with empty files.""" + winnowing = Winnowing(debug=True) + content = b'' + wfp = winnowing.wfp_for_contents('empty.txt', False, content) + + print(f'Empty file WFP:\n{wfp}') + + # Empty files should not generate fh2 + self.assertNotIn('fh2=', wfp) + + def test_no_line_endings_fh2(self): + """Test files without any line endings.""" + winnowing = Winnowing(debug=True) + content = b'no line endings here' + wfp = winnowing.wfp_for_contents('noline.txt', False, content) + + print(f'No line endings WFP:\n{wfp}') + + # Files without line endings should not generate fh2 + self.assertNotIn('fh2=', wfp) + + def test_all_platforms_generate_fh2(self): + """Test that all platforms generate fh2 when line endings are present.""" + winnowing = Winnowing(debug=True) + content = b'line1\nline2\n' + wfp = winnowing.wfp_for_contents('test.txt', False, content) + + print(f'Platform-independent WFP:\n{wfp}') + + # Any platform should generate fh2 when line endings are present + self.assertIn('fh2=', wfp) + + def test_verify_opposite_hash_calculation(self): + """Test that the opposite hash calculation works correctly.""" + winnowing = Winnowing(debug=True) + + # Test LF -> CRLF conversion + content_lf = b'line1\nline2\nline3\n' + wfp_lf = winnowing.wfp_for_contents('test_lf.txt', False, content_lf) + + # Test CRLF -> LF conversion + content_crlf = b'line1\r\nline2\r\nline3\r\n' + wfp_crlf = winnowing.wfp_for_contents('test_crlf.txt', False, content_crlf) + + print(f'LF content WFP:\n{wfp_lf}') + print(f'CRLF content WFP:\n{wfp_crlf}') + + # Both should generate fh2 + self.assertIn('fh2=', wfp_lf) + self.assertIn('fh2=', wfp_crlf) + + # Extract fh2 values + lf_fh2 = wfp_lf.split('fh2=')[1].split('\n')[0] + crlf_fh2 = wfp_crlf.split('fh2=')[1].split('\n')[0] + + # The fh2 values should be swapped (LF file gets CRLF hash, CRLF file gets LF hash) + import hashlib + expected_lf_to_crlf = hashlib.md5(content_lf.replace(b'\n', b'\r\n')).hexdigest() + expected_crlf_to_lf = hashlib.md5(content_crlf.replace(b'\r\n', b'\n')).hexdigest() + + self.assertEqual(lf_fh2, expected_lf_to_crlf) + self.assertEqual(crlf_fh2, expected_crlf_to_lf) + + def test_binary_file_with_line_endings(self): + """Test binary files with embedded line endings.""" + winnowing = Winnowing(debug=True) + # Binary content with embedded newlines + content = b'\x00\x01\n\x02\x03\r\n\x04' + wfp = winnowing.wfp_for_contents('binary.bin', True, content) + + print(f'Binary file WFP:\n{wfp}') + + # Binary files should not generate fh2 + self.assertNotIn('fh2=', wfp) + + def test_cr_only_line_endings(self): + """Test classic Mac CR-only line endings.""" + winnowing = Winnowing(debug=True) + content = b'line1\rline2\rline3\r' + wfp = winnowing.wfp_for_contents('mac.txt', False, content) + + print(f'CR-only WFP:\n{wfp}') + + # Should generate fh2 (platform independent) + self.assertIn('fh2=', wfp) + + # Should normalize CR to CRLF for the opposite hash + import hashlib + expected = content.replace(b'\r', b'\r\n') + expected_hash = hashlib.md5(expected).hexdigest() + self.assertIn(f'fh2={expected_hash}', wfp) + + def test_whitespace_only_file(self): + """Test files with only whitespace characters.""" + winnowing = Winnowing(debug=True) + content = b' \n\t\n \n' + wfp = winnowing.wfp_for_contents('whitespace.txt', False, content) + + print(f'Whitespace-only WFP:\n{wfp}') + + # Should generate fh2 since it has line endings + self.assertIn('fh2=', wfp) + + def test_mixed_complex_line_endings(self): + """Test complex mixed line ending scenarios.""" + winnowing = Winnowing(debug=True) + # Mix of CRLF, LF, and CR + content = b'line1\r\nline2\nline3\rline4\r\nline5\n' + wfp = winnowing.wfp_for_contents('mixed.txt', False, content) + + print(f'Mixed line endings WFP:\n{wfp}') + + # Should generate fh2 + self.assertIn('fh2=', wfp) + + # Verify the hash calculation + import hashlib + normalized = content.replace(b'\r\n', b'\n').replace(b'\r', b'\n') + expected_crlf = normalized.replace(b'\n', b'\r\n') + expected_hash = hashlib.md5(expected_crlf).hexdigest() + self.assertIn(f'fh2={expected_hash}', wfp) + + def test_fh2_with_skip_snippets(self): + """Test fh2 generation when skip_snippets is enabled.""" + winnowing = Winnowing(debug=True, skip_snippets=True) + content = b'line1\nline2\nline3\n' + wfp = winnowing.wfp_for_contents('test.txt', False, content) + + print(f'Skip snippets WFP:\n{wfp}') + + # Should still generate fh2 even when skipping snippets + self.assertIn('fh2=', wfp) + # But should not contain snippet fingerprints (line numbers) + lines = wfp.split('\n') + snippet_lines = [line for line in lines if '=' in line and line[0].isdigit()] + self.assertEqual(len(snippet_lines), 0) + + def test_fh2_with_obfuscation(self): + """Test fh2 generation with obfuscation enabled.""" + winnowing = Winnowing(debug=True, obfuscate=True) + content = b'line1\nline2\nline3\n' + wfp = winnowing.wfp_for_contents('test.txt', False, content) + + print(f'Obfuscated WFP:\n{wfp}') + + # Should still generate fh2 with obfuscation + self.assertIn('fh2=', wfp) + # Filename should be obfuscated + self.assertIn('1.txt', wfp) + self.assertNotIn('test.txt', wfp) + + def test_large_file_with_line_endings(self): + """Test large files with many line endings.""" + winnowing = Winnowing(debug=True, size_limit=True, post_size=1) # 1KB limit + # Create content larger than the limit + content = b'line\n' * 1000 # Should exceed 1KB + wfp = winnowing.wfp_for_contents('large.txt', False, content) + + print(f'Large file WFP length: {len(wfp)}') + + # Should still generate fh2 even with size limits + self.assertIn('fh2=', wfp) + + def test_single_line_no_newline(self): + """Test single line files without trailing newline.""" + winnowing = Winnowing(debug=True) + content = b'single line without newline' + wfp = winnowing.wfp_for_contents('single.txt', False, content) + + print(f'Single line no newline WFP:\n{wfp}') + + # Should not generate fh2 (no line endings) + self.assertNotIn('fh2=', wfp) + + def test_file_with_null_bytes_and_newlines(self): + """Test files with null bytes mixed with newlines.""" + winnowing = Winnowing(debug=True) + content = b'line1\x00\nline2\x00\x00\nline3\n' + wfp = winnowing.wfp_for_contents('nullbytes.txt', False, content) + + print(f'Null bytes with newlines WFP:\n{wfp}') + + # Should generate fh2 (has line endings) + self.assertIn('fh2=', wfp) + + def test_skip_headers_flag(self): + """Test skip_headers flag functionality.""" + # Sample Python file with headers, imports, and implementation + test_content = b"""# Copyright 2024 SCANOSS +# Licensed under MIT License +# All rights reserved + +import os +import sys +import json +from pathlib import Path + +def function1(): + data = {"key": "value"} + return json.dumps(data) + +def function2(): + path = Path("/tmp") + return str(path) + +class MyClass: + def __init__(self): + self.data = [] + + def add_item(self, item): + self.data.append(item) +""" + + # Test WITHOUT skip_headers + winnowing_no_skip = Winnowing(debug=False, skip_headers=False) + wfp_no_skip = winnowing_no_skip.wfp_for_contents('test.py', False, test_content) + + # Test WITH skip_headers + winnowing_skip = Winnowing(debug=False, skip_headers=True) + wfp_skip = winnowing_skip.wfp_for_contents('test.py', False, test_content) + + print(f'WFP without skip_headers:\n{wfp_no_skip}') + print(f'\nWFP with skip_headers:\n{wfp_skip}') + + # Both should have file= line + self.assertIn('file=', wfp_no_skip) + self.assertIn('file=', wfp_skip) + + # Extract snippet line numbers from both WFPs + def extract_line_numbers(wfp): + lines = wfp.split('\n') + line_numbers = [] + for line in lines: + if '=' in line and line.split('=')[0].isdigit(): + line_numbers.append(int(line.split('=')[0])) + return line_numbers + + lines_no_skip = extract_line_numbers(wfp_no_skip) + lines_skip = extract_line_numbers(wfp_skip) + + # Both should have snippet lines + self.assertGreater(len(lines_no_skip), 0, "Should have snippets without skip_headers") + self.assertGreater(len(lines_skip), 0, "Should have snippets with skip_headers") + + # First line number with skip_headers should be HIGHER (skipped headers/imports) + # Line 10 in the content is "def function1():" which is where real code starts + min_line_no_skip = min(lines_no_skip) + min_line_skip = min(lines_skip) + + print(f'First snippet line without skip_headers: {min_line_no_skip}') + print(f'First snippet line with skip_headers: {min_line_skip}') + + # With skip_headers, first line should be after imports (around line 10+) + # Without skip_headers, first line should be earlier (around line 5-8) + self.assertGreater( + min_line_skip, + min_line_no_skip, + "skip_headers should result in higher starting line number" + ) + + # Verify line 10+ (implementation) appears in skip_headers output + self.assertGreaterEqual( + min_line_skip, + 10, + "With skip_headers, snippets should start at implementation (line 10+)" + ) + + # Verify start_line tag is present in skip_headers output + self.assertIn('start_line=', wfp_skip, "start_line tag should be present with skip_headers") + self.assertNotIn('start_line=', wfp_no_skip, "start_line tag should NOT be present without skip_headers") + + # Extract and validate start_line value + start_line_value = None + for line in wfp_skip.split('\n'): + if line.startswith('start_line='): + start_line_value = int(line.split('=')[1]) + break + + self.assertIsNotNone(start_line_value, "start_line value should be found") + self.assertGreater(start_line_value, 0, "start_line should indicate skipped lines") + print(f'start_line tag value: {start_line_value}') + + def test_skip_headers_with_different_languages(self): + """Test skip_headers with different programming languages.""" + + # JavaScript test + js_content = b"""/* + * Copyright 2024 + * Licensed under MIT + */ + +import React from 'react'; +import { Component } from 'react'; + +class App extends Component { + render() { + return
Hello
; + } +} +""" + winnowing_js = Winnowing(debug=False, skip_headers=True) + wfp_js = winnowing_js.wfp_for_contents('test.js', False, js_content) + + print(f'JavaScript WFP with skip_headers:\n{wfp_js}') + + # Should have snippets starting from class definition (not imports) + self.assertIn('file=', wfp_js) + + # Go test + go_content = b"""// Copyright 2024 +// Licensed under MIT + +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Println("Hello") +} +""" + winnowing_go = Winnowing(debug=False, skip_headers=True) + wfp_go = winnowing_go.wfp_for_contents('test.go', False, go_content) + + print(f'Go WFP with skip_headers:\n{wfp_go}') + + # Should have snippets starting from func main (not package/imports) + self.assertIn('file=', wfp_go) \ No newline at end of file diff --git a/tools/get_next_version.sh b/tools/get_next_version.sh new file mode 100755 index 00000000..d0d299f0 --- /dev/null +++ b/tools/get_next_version.sh @@ -0,0 +1,59 @@ +#!/bin/bash +### +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2024, SCANOSS +# +# 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. +### +# +# Get the defined package version and compare to the latest tag. Echo the new tag if it doesn't already exist. +# +export d=$(dirname "$0") +if [ "$d" = "" ] ; then + export d=. +fi + +# Get latest git tagged version +version=$(git describe --tags --abbrev=0) +if [[ -z "$version" ]] ; then + version=$(git describe --tags "$(git rev-list --tags --max-count=1)") +fi +if [[ -z "$version" ]] ; then + echo "Error: Failed to determine a valid version number" >&2 + exit 1 +fi +# Get Python package version +python_version=$($d/../version.py) +if [ $? -eq 1 ] || [[ "$python_version" = "" ]]; then + echo "Error: failed to get python app version." + exit 1 +fi +# Convert to semver (with 'v' prefix) +semver_python="v$python_version" + +echo "Latest Tag: $version, Python Version: $python_version" >&2 + +# If the two versions are the same abort, as we don't want to apply the same tag again +if [[ "$version" == "$semver_python" ]] ; then + echo "Latest tag and python version are the same: $version" >&2 + exit 1 +fi +echo "$semver_python" +exit 0 diff --git a/tools/linter.sh b/tools/linter.sh new file mode 100755 index 00000000..bb8a8413 --- /dev/null +++ b/tools/linter.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MIT +# +# Lint Python files changed since merge base with origin/main +# Usage: linter.sh [--fix] [--docker] [--all] +set -e +RUFF_IMAGE="ghcr.io/astral-sh/ruff:0.14.2" +# Parse arguments +FIX_FLAG="" +USE_DOCKER=false +ALL_FILES=false +while [[ $# -gt 0 ]]; do + case $1 in + --fix) + FIX_FLAG="--fix" + shift + ;; + --docker) + USE_DOCKER=true + shift + ;; + --all) + ALL_FILES=true + shift + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [--fix] [--docker] [--all]" + exit 1 + ;; + esac +done +# Get a list of files to analyse +files="" +if [ "$ALL_FILES" = "true" ] ; then + echo "Analysing all python files..." + files=$(find . -type f -name "*.py" -print) +else + # Find merge base with origin/main + if ! git rev-parse --verify origin/main >/dev/null 2>&1; then + echo "Error: origin/main branch not found. Ensure you have fetched from origin." + exit 1 + fi + merge_base=$(git merge-base origin/main HEAD) + # Get all changed Python files since merge base + files=$(git diff --name-only "$merge_base" HEAD | grep '\.py$' || true) +fi +# Filter out files that match exclude patterns from pyproject.toml +# this is a temporary workaround until we fix all the lint errors +filtered_files=$(echo "$files" | grep -v -E 'tests/|test_.*\.py|src/protoc_gen_swagger/|src/scanoss/api/' || true) + +# Check if there are any Python files changed +if [ -z "$filtered_files" ]; then + echo "No Python files changed" + exit 0 +fi +file_count=$(echo "${filtered_files}" | wc -l | tr -d ' ') +echo "Analysing ${file_count} files..." +# Run linter +if [ "$USE_DOCKER" = true ]; then + # Run with Docker + echo "$filtered_files" | xargs -r docker run --rm -v "$(pwd)":/src -w /src ${RUFF_IMAGE} check ${FIX_FLAG} +else + # Run locally + echo "$filtered_files" | xargs -r python3 -m ruff check ${FIX_FLAG} +fi \ No newline at end of file diff --git a/version.py b/version.py index 51d764ab..1ee8cfbf 100755 --- a/version.py +++ b/version.py @@ -1,29 +1,30 @@ #!/usr/bin/env python3 """ - SPDX-License-Identifier: MIT - - Copyright (c) 2021, SCANOSS - - 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. +SPDX-License-Identifier: MIT + + Copyright (c) 2021, SCANOSS + + 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. """ -import os + import codecs +import os def read(rel_path): @@ -47,12 +48,11 @@ def get_version(rel_path): if line.startswith('__version__'): delim = '"' if '"' in line else "'" return line.split(delim)[1] - else: - raise RuntimeError("Unable to find version string.") + raise RuntimeError('Unable to find version string.') """ Load __init__.py from the scanoss package and print the version to stdout """ -if __name__ == "__main__": +if __name__ == '__main__': print(get_version('src/scanoss/__init__.py'))