diff --git a/.github/workflows/release-branch.yml b/.github/workflows/release-branch.yml index f2e1c31e..cc7cb92f 100644 --- a/.github/workflows/release-branch.yml +++ b/.github/workflows/release-branch.yml @@ -29,6 +29,15 @@ jobs: run: ./docker-scout quickview alpine:latest - name: TEST docker scout cves run: ./docker-scout cves docker/scout-demo-service:main + - name: TEST docker scout recommendations + run: ./docker-scout recommendations docker/scout-demo-service:main + - name: Watch repository after release + run: | + ./docker-scout watch \ + --org my-org \ + --repository registry.example.com/scout-demo-service \ + --tag latest \ + --dry-run - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx diff --git a/.github/workflows/security_scan.yml b/.github/workflows/security_scan.yml new file mode 100644 index 00000000..8a723eb0 --- /dev/null +++ b/.github/workflows/security_scan.yml @@ -0,0 +1,31 @@ +name: Weekly Security Scan + +on: + schedule: + - cron: '0 8 * * 1' + workflow_dispatch: + +jobs: + security: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: pip install pytest pytest-html pyyaml + + - name: Run YAML schema tests + run: pytest tests/ --html=security-report.html --self-contained-html + + - name: Upload security report + if: always() + uses: actions/upload-artifact@v4 + with: + name: security-report + path: security-report.html \ No newline at end of file diff --git a/.github/workflows/validate_yaml.yml b/.github/workflows/validate_yaml.yml new file mode 100644 index 00000000..43c1f057 --- /dev/null +++ b/.github/workflows/validate_yaml.yml @@ -0,0 +1,38 @@ +name: Validate YAML Schema + +on: + push: + paths: + - 'docs/**.yaml' + - 'tests/**' + pull_request: + paths: + - 'docs/**.yaml' + - 'tests/**' + + workflow_dispatch: + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: pip install pytest pytest-html pyyaml + + - name: Run YAML schema tests + run: pytest tests/ --html=reporte.html --self-contained-html + + - name: Upload test report + if: always() + uses: actions/upload-artifact@v4 + with: + name: reporte-pruebas + path: reporte.html \ No newline at end of file diff --git a/README.md b/README.md index 2bd0ee7f..43815277 100644 --- a/README.md +++ b/README.md @@ -1,401 +1,403 @@ -- [Docker Scout](#docker-scout) -- [Usage](#usage) -- [CLI Plugin Installation](#cli-plugin-installation) -- [Run as container](#run-as-container) -- [CI integration](#ci-integration) -- [License](#license) - -# Docker Scout - -[Docker Scout](https://www.docker.com/products/docker-scout/) is a set of software supply chain features integrated into Docker's user interfaces and command line interface (CLI). These features offer comprehensive visibility into the structure and security of container images. -This repository contains installable binaries of the `docker scout` CLI plugin. - -## Usage - -The [CLI documentation is available in this repository](./docs/scout.md). - -See the [reference documentation](https://docs.docker.com/scout) to learn about Docker Scout including Docker Desktop and Docker Hub integrations. - -### Environment Variables - -The following environment variables are available to configure the Scout CLI: - -| Name | Format | Description | -| ---- | ------ | ----------- | -| `DOCKER_SCOUT_CACHE_FORMAT` | String | Format of the local image cache; can be `oci` or `tar` | -| `DOCKER_SCOUT_CACHE_DIR` | String | Directory where the local SBOM cache is stored | -| `DOCKER_SCOUT_NO_CACHE` | Boolean | Disable the local SBOM cache | -| `DOCKER_SCOUT_OFFLINE` | Boolean | Offline mode during SBOM indexing | -| `DOCKER_SCOUT_REGISTRY_TOKEN` | String | Registry Access token to authenticate when pulling images | -| `DOCKER_SCOUT_REGISTRY_USER` | String | Registry user name to authenticate when pulling images | -| `DOCKER_SCOUT_REGISTRY_PASSWORD` | String | Registry password/PAT to authenticate when pulling images | -| `DOCKER_SCOUT_HUB_USER` | String | Docker Hub user name to authenticate against the Docker Scout backend | -| `DOCKER_SCOUT_HUB_PASSWORD` | String | Docker Hub password/PAT to authenticate against the Docker Scout backend | -| `DOCKER_SCOUT_NEW_VERSION_WARN` | Boolean | Warn about new versions of the Docker Scout CLI | -| `DOCKER_SCOUT_EXPERIMENTAL_WARN` | Boolean | Warn about experimental features | -| `DOCKER_SCOUT_EXPERIMENTAL_POLICY_OUTPUT` | Boolean | Disable experimental policy output | - -You can found further information about environment variables [here](https://docs.docker.com/scout/how-tos/configure-cli/). - - -## CLI Plugin Installation - -### Docker Desktop - -`docker scout` CLI plugin is available by default on [Docker Desktop](https://docs.docker.com/desktop/) starting with version `4.17`. - -### Manual Installation - -To install it manually: - -- Download the `docker-scout` binary corresponding to your platform from the [latest](https://github.com/docker/scout-cli/releases/latest) or [other](https://github.com/docker/scout-cli/releases) releases. -- Uncompress it as - - `docker-scout` on _Linux_ and _macOS_ - - `docker-scout.exe` on _Windows_ -- Copy the binary to the `scout` directory - - `$HOME/.docker/scout` on _Linux_ and _macOS_ - - `%USERPROFILE%\.docker\scout` on _Windows_ -- Make it executable on _Linux_ and _macOS_ - - `chmod +x $HOME/.docker/scout/docker-scout` -- Authorize the binary to be executable on _macOS_ - - `xattr -d com.apple.quarantine $HOME/.docker/scout/docker-scout` -- Add the `scout` directory to your `.docker/config.json` as a plugin directory - - `$HOME/.docker/config.json` on _Linux_ and _macOS_ - - `%USERPROFILE%\.docker\config.json` on _Windows_ - - Add the `cliPluginsExtraDirs` property to the `config.json` file -``` -{ - ... - "cliPluginsExtraDirs": [ - "" - ], - ... -} -``` - -### Script Installation (macOS and Linux) - -To install, run the following command in your terminal: - -```shell -curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s -- -``` - -## Run as container - -A container image to run the Docker Scout CLI in containerized environments is available at [docker/scout-cli](https://hub.docker.com/r/docker/scout-cli). - -## CI Integration - -Docker Scout CLI can be used in CI environments. See below for the various ways to integrate the CLI into your CI pipelines. - -### GitHub Action - -An early prototype of running the Docker Scout CLI as part of a GitHub Action workflow is available at [docker/scout-action](https://github.com/docker/scout-action). - -The following GitHub Action workflow can be used as a template to integrate Docker Scout: - -```yaml -name: Docker - -on: - push: - tags: [ "*" ] - branches: - - 'main' - pull_request: - branches: [ "**" ] - -env: - # Use docker.io for Docker Hub if empty - REGISTRY: docker.io - IMAGE_NAME: ${{ github.repository }} - SHA: ${{ github.event.pull_request.head.sha || github.event.after }} - -jobs: - build: - - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - ref: ${{ env.SHA }} - - - name: Setup Docker buildx - uses: docker/setup-buildx-action@v2.5.0 - - # Login against a Docker registry except on PR - # https://github.com/docker/login-action - - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@v2.1.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ secrets.DOCKER_USER }} - password: ${{ secrets.DOCKER_PAT }} - - # Extract metadata (tags, labels) for Docker - # https://github.com/docker/metadata-action - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@v4.4.0 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - labels: | - org.opencontainers.image.revision=${{ env.SHA }} - tags: | - type=edge,branch=$repo.default_branch - type=semver,pattern=v{{version}} - type=sha,prefix=,suffix=,format=short - - # Build and push Docker image with Buildx (don't push on PR) - # https://github.com/docker/build-push-action - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@v4.0.0 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Docker Scout - id: docker-scout - if: ${{ github.event_name == 'pull_request' }} - uses: docker/scout-action@dd36f5b0295baffa006aa6623371f226cc03e506 - with: - command: cves - image: ${{ steps.meta.outputs.tags }} - only-severities: critical,high - exit-code: true -``` - -### GitLab - -Use the following pipeline definition as a template to get Docker Scout integrated in GitLab CI: - -```yaml -docker-build: - image: docker:latest - stage: build - services: - - docker:dind - before_script: - - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY - - # Install curl and the Docker Scout CLI - - | - apk add --update curl - curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s -- - apk del curl - rm -rf /var/cache/apk/* - # Login to Docker Hub required for Docker Scout CLI - - echo "$DOCKER_HUB_PAT" | docker login --username "$DOCKER_HUB_USER" --password-stdin - script: - - | - if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then - tag="" - echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'" - else - tag=":$CI_COMMIT_REF_SLUG" - echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag" - fi - - docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" . - - - | - if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then - # Get a CVE report for the built image and fail the pipeline when critical or high CVEs are detected - docker scout cves "$CI_REGISTRY_IMAGE${tag}" --exit-code --only-severity critical,high - else - # Compare image from branch with latest image from the default branch and fail if new critical or high CVEs are detected - docker scout compare "$CI_REGISTRY_IMAGE${tag}" --to "$CI_REGISTRY_IMAGE:latest" --exit-on vulnerability,policy --only-severity critical,high --ignore-unchanged - fi - - - docker push "$CI_REGISTRY_IMAGE${tag}" - rules: - - if: $CI_COMMIT_BRANCH - exists: - - Dockerfile -``` - -### CircleCI - -Use the following pipeline definition as a template to get Docker Scout integrated in CircleCI project: - -```yaml -version: 2.1 - -jobs: - - build: - - docker: - - image: cimg/base:stable - - environment: - IMAGE_TAG: docker/scout-demo-service:latest - - steps: - # Checkout the repository files - - checkout - - # Set up a separate Docker environment to run `docker` commands in - - setup_remote_docker: - version: 20.10.24 - - # Install Docker Scout and login to Docker Hub - - run: - name: Install Docker Scout - command: | - env - curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s -- -b /home/circleci/bin - echo $DOCKER_HUB_PAT | docker login -u $DOCKER_HUB_USER --password-stdin - - # Build the Docker image - - run: - name: Build Docker image - command: docker build -t $IMAGE_TAG . - - # Run Docker Scout - - run: - name: Scan image for CVEs - command: | - docker-scout cves $IMAGE_TAG --exit-code --only-severity critical,high - -workflows: - build-docker-image: - jobs: - - build -``` - -### Microsoft Azure DevOps Pipelines - -Use the following pipeline definition as a template to get Docker Scout integrated in Azure DevOps Pipelines: - -```yaml -trigger: -- main - -resources: -- repo: self - -variables: - tag: '$(Build.BuildId)' - image: 'vonwig/nodejs-service' - -stages: -- stage: Build - displayName: Build image - jobs: - - job: Build - displayName: Build - pool: - vmImage: ubuntu-latest - steps: - - task: Docker@2 - displayName: Build an image - inputs: - command: build - dockerfile: '$(Build.SourcesDirectory)/Dockerfile' - repository: $(image) - tags: | - $(tag) - - task: CmdLine@2 - displayName: Find CVEs on image - inputs: - script: | - # Install the Docker Scout CLI - curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s -- - # Login to Docker Hub required for Docker Scout CLI - docker login -u $(DOCKER_HUB_USER) -p $(DOCKER_HUB_PAT) - # Get a CVE report for the built image and fail the pipeline when critical or high CVEs are detected - docker scout cves $(image):$(tag) --exit-code --only-severity critical,high -``` - -### Jenkins - -The following snippet can be added to a `Jenkinsfile` to install and analyze images: - -```groovy - stage('Analyze image') { - steps { - // Install Docker Scout - sh 'curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s -- -b /usr/local/bin' - - // Log into Docker Hub - sh 'echo $DOCKER_HUB_PAT | docker login -u $DOCKER_HUB_USER --password-stdin' - - // Analyze and fail on critical or high vulnerabilities - sh 'docker-scout cves $IMAGE_TAG --exit-code --only-severity critical,high' - } - } -``` - -This example assume two secrets to be available to authenticate against Docker Hub, called `DOCKER_HUB_USER` and `DOCKER_HUB_PAT`. - -### Bitbucket - -Use the following pipeline definition as a template to get Docker Scout integrated in Bitbucket Pipelines: - -```yaml -image: docker - -pipelines: - default: - - step: - name: Build - services: - - docker - caches: - - docker - script: - - echo "$DOCKER_HUB_PAT" | docker login --username "$DOCKER_HUB_USER" --password-stdin $CI_REGISTRY - - # Install curl and the Docker Scout CLI - - | - export DOCKER_BUILDKIT=0 - apk add --update curl - curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s -- - apk del curl - rm -rf /var/cache/apk/* - # Login to Docker Hub required for Docker Scout CLI - - echo "$DOCKER_HUB_PAT" | docker login --username "$DOCKER_HUB_USER" --password-stdin - - - | - export DEVELOPMENT_BRANCH="main" - if [[ "$BITBUCKET_BRANCH" == "$DEVELOPMENT_BRANCH" ]]; then # Bitbucket uses master by default, adjust if your default branch is different - tag=":latest" - echo "Running on default branch '$DEVELOPMENT_BRANCH': tag = 'latest'" - else - tag=":$BITBUCKET_COMMIT" - echo "Running on branch '$BITBUCKET_BRANCH': tag = $tag" - fi - - docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" . - - - | - if [[ "$BITBUCKET_BRANCH" == "$DEVELOPMENT_BRANCH" ]]; then - # Get a CVE report for the built image and fail the pipeline when critical or high CVEs are detected - docker scout cves "$CI_REGISTRY_IMAGE${tag}" --exit-code --only-severity critical,high - else - # Compare image from branch with latest image from the default branch and fail if new critical or high CVEs are detected - docker scout compare "$CI_REGISTRY_IMAGE${tag}" --to "$CI_REGISTRY_IMAGE:latest" --exit-on vulnerability,policy --only-severity critical,high --ignore-unchanged - fi - - docker push "$CI_REGISTRY_IMAGE${tag}" - -definitions: - services: - docker: - memory: 2048 # Optional: Increase if needed -``` - -This example assumes two secrets to be available to authenticate against Docker Hub, called `DOCKER_HUB_USER` and `DOCKER_HUB_PAT`, also is necessary more two secrets called `CI_REGISTRY`, `CI_REGISTRY_IMAGE` about registry info. - -## License - -The Docker Scout CLI is licensed under the Terms and Conditions of the [Docker Subscription Service Agreement](https://www.docker.com/legal/docker-subscription-service-agreement/). +- [Docker Scout](#docker-scout) +- [Usage](#usage) +- [CLI Plugin Installation](#cli-plugin-installation) +- [Run as container](#run-as-container) +- [CI integration](#ci-integration) +- [License](#license) + +# Docker Scout + +[Docker Scout](https://www.docker.com/products/docker-scout/) is a set of software supply chain features integrated into Docker's user interfaces and command line interface (CLI). These features offer comprehensive visibility into the structure and security of container images. +This repository contains installable binaries of the `docker scout` CLI plugin. + +## Usage + +The [CLI documentation is available in this repository](./docs/scout.md). + +See the [reference documentation](https://docs.docker.com/scout) to learn about Docker Scout including Docker Desktop and Docker Hub integrations. + +### Environment Variables + +The following environment variables are available to configure the Scout CLI: + +| Name | Format | Description | +| ---- | ------ | ----------- | +| `DOCKER_SCOUT_CACHE_FORMAT` | String | Format of the local image cache; can be `oci` or `tar` | +| `DOCKER_SCOUT_CACHE_DIR` | String | Directory where the local SBOM cache is stored | +| `DOCKER_SCOUT_NO_CACHE` | Boolean | Disable the local SBOM cache | +| `DOCKER_SCOUT_OFFLINE` | Boolean | Offline mode during SBOM indexing | +| `DOCKER_SCOUT_REGISTRY_TOKEN` | String | Registry Access token to authenticate when pulling images | +| `DOCKER_SCOUT_REGISTRY_USER` | String | Registry user name to authenticate when pulling images | +| `DOCKER_SCOUT_REGISTRY_PASSWORD` | String | Registry password/PAT to authenticate when pulling images | +| `DOCKER_SCOUT_HUB_USER` | String | Docker Hub user name to authenticate against the Docker Scout backend | +| `DOCKER_SCOUT_HUB_PASSWORD` | String | Docker Hub password/PAT to authenticate against the Docker Scout backend | +| `DOCKER_SCOUT_NEW_VERSION_WARN` | Boolean | Warn about new versions of the Docker Scout CLI | +| `DOCKER_SCOUT_EXPERIMENTAL_WARN` | Boolean | Warn about experimental features | +| `DOCKER_SCOUT_EXPERIMENTAL_POLICY_OUTPUT` | Boolean | Disable experimental policy output | + +You can found further information about environment variables [here](https://docs.docker.com/scout/how-tos/configure-cli/). + + +## CLI Plugin Installation + +### Docker Desktop + +`docker scout` CLI plugin is available by default on [Docker Desktop](https://docs.docker.com/desktop/) starting with version `4.17`. + +### Manual Installation + +To install it manually: + +- Download the `docker-scout` binary corresponding to your platform from the [latest](https://github.com/docker/scout-cli/releases/latest) or [other](https://github.com/docker/scout-cli/releases) releases. +- Uncompress it as + - `docker-scout` on _Linux_ and _macOS_ + - `docker-scout.exe` on _Windows_ +- Copy the binary to the `scout` directory + - `$HOME/.docker/scout` on _Linux_ and _macOS_ + - `%USERPROFILE%\.docker\scout` on _Windows_ +- Make it executable on _Linux_ and _macOS_ + - `chmod +x $HOME/.docker/scout/docker-scout` +- Authorize the binary to be executable on _macOS_ + - `xattr -d com.apple.quarantine $HOME/.docker/scout/docker-scout` +- Add the `scout` directory to your `.docker/config.json` as a plugin directory + - `$HOME/.docker/config.json` on _Linux_ and _macOS_ + - `%USERPROFILE%\.docker\config.json` on _Windows_ + - Add the `cliPluginsExtraDirs` property to the `config.json` file +``` +{ + ... + "cliPluginsExtraDirs": [ + "" + ], + ... +} +``` + +### Script Installation (macOS and Linux) + +To install, run the following command in your terminal: + +```shell +curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s -- +``` + +## Run as container + +A container image to run the Docker Scout CLI in containerized environments is available at [docker/scout-cli](https://hub.docker.com/r/docker/scout-cli). + +## CI Integration + +Docker Scout CLI can be used in CI environments. See below for the various ways to integrate the CLI into your CI pipelines. + +### GitHub Action + +An early prototype of running the Docker Scout CLI as part of a GitHub Action workflow is available at [docker/scout-action](https://github.com/docker/scout-action). + +The following GitHub Action workflow can be used as a template to integrate Docker Scout: + +```yaml +name: Docker + +on: + push: + tags: [ "*" ] + branches: + - 'main' + pull_request: + branches: [ "**" ] + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: docker.io + IMAGE_NAME: ${{ github.repository }} + SHA: ${{ github.event.pull_request.head.sha || github.event.after }} + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + ref: ${{ env.SHA }} + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v2.5.0 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@v2.1.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PAT }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v4.4.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + labels: | + org.opencontainers.image.revision=${{ env.SHA }} + tags: | + type=edge,branch=$repo.default_branch + type=semver,pattern=v{{version}} + type=sha,prefix=,suffix=,format=short + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v4.0.0 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Docker Scout + id: docker-scout + if: ${{ github.event_name == 'pull_request' }} + uses: docker/scout-action@dd36f5b0295baffa006aa6623371f226cc03e506 + with: + command: cves + image: ${{ steps.meta.outputs.tags }} + only-severities: critical,high + exit-code: true +``` + +### GitLab + +Use the following pipeline definition as a template to get Docker Scout integrated in GitLab CI: + +```yaml +docker-build: + image: docker:latest + stage: build + services: + - docker:dind + before_script: + - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + + # Install curl and the Docker Scout CLI + - | + apk add --update curl + curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s -- + apk del curl + rm -rf /var/cache/apk/* + # Login to Docker Hub required for Docker Scout CLI + - echo "$DOCKER_HUB_PAT" | docker login --username "$DOCKER_HUB_USER" --password-stdin + script: + - | + if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then + tag="" + echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'" + else + tag=":$CI_COMMIT_REF_SLUG" + echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag" + fi + - docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" . + + - | + if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then + # Get a CVE report for the built image and fail the pipeline when critical or high CVEs are detected + docker scout cves "$CI_REGISTRY_IMAGE${tag}" --exit-code --only-severity critical,high + else + # Compare image from branch with latest image from the default branch and fail if new critical or high CVEs are detected + docker scout compare "$CI_REGISTRY_IMAGE${tag}" --to "$CI_REGISTRY_IMAGE:latest" --exit-on vulnerability,policy --only-severity critical,high --ignore-unchanged + fi + + - docker push "$CI_REGISTRY_IMAGE${tag}" + rules: + - if: $CI_COMMIT_BRANCH + exists: + - Dockerfile +``` + +### CircleCI + +Use the following pipeline definition as a template to get Docker Scout integrated in CircleCI project: + +```yaml +version: 2.1 + +jobs: + + build: + + docker: + - image: cimg/base:stable + + environment: + IMAGE_TAG: docker/scout-demo-service:latest + + steps: + # Checkout the repository files + - checkout + + # Set up a separate Docker environment to run `docker` commands in + - setup_remote_docker: + version: 20.10.24 + + # Install Docker Scout and login to Docker Hub + - run: + name: Install Docker Scout + command: | + env + curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s -- -b /home/circleci/bin + echo $DOCKER_HUB_PAT | docker login -u $DOCKER_HUB_USER --password-stdin + + # Build the Docker image + - run: + name: Build Docker image + command: docker build -t $IMAGE_TAG . + + # Run Docker Scout + - run: + name: Scan image for CVEs + command: | + docker-scout cves $IMAGE_TAG --exit-code --only-severity critical,high + +workflows: + build-docker-image: + jobs: + - build +``` + +### Microsoft Azure DevOps Pipelines + +Use the following pipeline definition as a template to get Docker Scout integrated in Azure DevOps Pipelines: + +```yaml +trigger: +- main + +resources: +- repo: self + +variables: + tag: '$(Build.BuildId)' + image: 'vonwig/nodejs-service' + +stages: +- stage: Build + displayName: Build image + jobs: + - job: Build + displayName: Build + pool: + vmImage: ubuntu-latest + steps: + - task: Docker@2 + displayName: Build an image + inputs: + command: build + dockerfile: '$(Build.SourcesDirectory)/Dockerfile' + repository: $(image) + tags: | + $(tag) + - task: CmdLine@2 + displayName: Find CVEs on image + inputs: + script: | + # Install the Docker Scout CLI + curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s -- + # Login to Docker Hub required for Docker Scout CLI + docker login -u $(DOCKER_HUB_USER) -p $(DOCKER_HUB_PAT) + # Get a CVE report for the built image and fail the pipeline when critical or high CVEs are detected + docker scout cves $(image):$(tag) --exit-code --only-severity critical,high +``` + +### Jenkins + +The following snippet can be added to a `Jenkinsfile` to install and analyze images: + +```groovy + stage('Analyze image') { + steps { + // Install Docker Scout + sh 'curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s -- -b /usr/local/bin' + + // Log into Docker Hub + sh 'echo $DOCKER_HUB_PAT | docker login -u $DOCKER_HUB_USER --password-stdin' + + // Analyze and fail on critical or high vulnerabilities + sh 'docker-scout cves $IMAGE_TAG --exit-code --only-severity critical,high' + } + } +``` + +This example assume two secrets to be available to authenticate against Docker Hub, called `DOCKER_HUB_USER` and `DOCKER_HUB_PAT`. + +### Bitbucket + +Use the following pipeline definition as a template to get Docker Scout integrated in Bitbucket Pipelines: + +```yaml +image: docker + +pipelines: + default: + - step: + name: Build + services: + - docker + caches: + - docker + script: + - echo "$DOCKER_HUB_PAT" | docker login --username "$DOCKER_HUB_USER" --password-stdin $CI_REGISTRY + + # Install curl and the Docker Scout CLI + - | + export DOCKER_BUILDKIT=0 + apk add --update curl + curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s -- + apk del curl + rm -rf /var/cache/apk/* + # Login to Docker Hub required for Docker Scout CLI + - echo "$DOCKER_HUB_PAT" | docker login --username "$DOCKER_HUB_USER" --password-stdin + + - | + export DEVELOPMENT_BRANCH="main" + if [[ "$BITBUCKET_BRANCH" == "$DEVELOPMENT_BRANCH" ]]; then # Bitbucket uses master by default, adjust if your default branch is different + tag=":latest" + echo "Running on default branch '$DEVELOPMENT_BRANCH': tag = 'latest'" + else + tag=":$BITBUCKET_COMMIT" + echo "Running on branch '$BITBUCKET_BRANCH': tag = $tag" + fi + - docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" . + + - | + if [[ "$BITBUCKET_BRANCH" == "$DEVELOPMENT_BRANCH" ]]; then + # Get a CVE report for the built image and fail the pipeline when critical or high CVEs are detected + docker scout cves "$CI_REGISTRY_IMAGE${tag}" --exit-code --only-severity critical,high + else + # Compare image from branch with latest image from the default branch and fail if new critical or high CVEs are detected + docker scout compare "$CI_REGISTRY_IMAGE${tag}" --to "$CI_REGISTRY_IMAGE:latest" --exit-on vulnerability,policy --only-severity critical,high --ignore-unchanged + fi + - docker push "$CI_REGISTRY_IMAGE${tag}" + +definitions: + services: + docker: + memory: 2048 # Optional: Increase if needed +``` + +This example assumes two secrets to be available to authenticate against Docker Hub, called `DOCKER_HUB_USER` and `DOCKER_HUB_PAT`, also is necessary more two secrets called `CI_REGISTRY`, `CI_REGISTRY_IMAGE` about registry info. + +## License + +The Docker Scout CLI is licensed under the Terms and Conditions of the [Docker Subscription Service Agreement](https://www.docker.com/legal/docker-subscription-service-agreement/). +# Test PR + \ No newline at end of file diff --git a/docs/assets/style.css b/docs/assets/style.css new file mode 100644 index 00000000..561524c6 --- /dev/null +++ b/docs/assets/style.css @@ -0,0 +1,319 @@ +body { + font-family: Helvetica, Arial, sans-serif; + font-size: 12px; + /* do not increase min-width as some may use split screens */ + min-width: 800px; + color: #999; +} + +h1 { + font-size: 24px; + color: black; +} + +h2 { + font-size: 16px; + color: black; +} + +p { + color: black; +} + +a { + color: #999; +} + +table { + border-collapse: collapse; +} + +/****************************** + * SUMMARY INFORMATION + ******************************/ +#environment td { + padding: 5px; + border: 1px solid #e6e6e6; + vertical-align: top; +} +#environment tr:nth-child(odd) { + background-color: #f6f6f6; +} +#environment ul { + margin: 0; + padding: 0 20px; +} + +/****************************** + * TEST RESULT COLORS + ******************************/ +span.passed, +.passed .col-result { + color: green; +} + +span.skipped, +span.xfailed, +span.rerun, +.skipped .col-result, +.xfailed .col-result, +.rerun .col-result { + color: orange; +} + +span.error, +span.failed, +span.xpassed, +.error .col-result, +.failed .col-result, +.xpassed .col-result { + color: red; +} + +.col-links__extra { + margin-right: 3px; +} + +/****************************** + * RESULTS TABLE + * + * 1. Table Layout + * 2. Extra + * 3. Sorting items + * + ******************************/ +/*------------------ + * 1. Table Layout + *------------------*/ +#results-table { + border: 1px solid #e6e6e6; + color: #999; + font-size: 12px; + width: 100%; +} +#results-table th, +#results-table td { + padding: 5px; + border: 1px solid #e6e6e6; + text-align: left; +} +#results-table th { + font-weight: bold; +} + +/*------------------ + * 2. Extra + *------------------*/ +.logwrapper { + max-height: 230px; + overflow-y: scroll; + background-color: #e6e6e6; +} +.logwrapper.expanded { + max-height: none; +} +.logwrapper.expanded .logexpander:after { + content: "collapse [-]"; +} +.logwrapper .logexpander { + z-index: 1; + position: sticky; + top: 10px; + width: max-content; + border: 1px solid; + border-radius: 3px; + padding: 5px 7px; + margin: 10px 0 10px calc(100% - 80px); + cursor: pointer; + background-color: #e6e6e6; +} +.logwrapper .logexpander:after { + content: "expand [+]"; +} +.logwrapper .logexpander:hover { + color: #000; + border-color: #000; +} +.logwrapper .log { + min-height: 40px; + position: relative; + top: -50px; + height: calc(100% + 50px); + border: 1px solid #e6e6e6; + color: black; + display: block; + font-family: "Courier New", Courier, monospace; + padding: 5px; + padding-right: 80px; + white-space: pre-wrap; +} + +div.media { + border: 1px solid #e6e6e6; + float: right; + height: 240px; + margin: 0 5px; + overflow: hidden; + width: 320px; +} + +.media-container { + display: grid; + grid-template-columns: 25px auto 25px; + align-items: center; + flex: 1 1; + overflow: hidden; + height: 200px; +} + +.media-container--fullscreen { + grid-template-columns: 0px auto 0px; +} + +.media-container__nav--right, +.media-container__nav--left { + text-align: center; + cursor: pointer; +} + +.media-container__viewport { + cursor: pointer; + text-align: center; + height: inherit; +} +.media-container__viewport img, +.media-container__viewport video { + object-fit: cover; + width: 100%; + max-height: 100%; +} + +.media__name, +.media__counter { + display: flex; + flex-direction: row; + justify-content: space-around; + flex: 0 0 25px; + align-items: center; +} + +.collapsible td:not(.col-links) { + cursor: pointer; +} +.collapsible td:not(.col-links):hover::after { + color: #bbb; + font-style: italic; + cursor: pointer; +} + +.col-result { + width: 130px; +} +.col-result:hover::after { + content: " (hide details)"; +} + +.col-result.collapsed:hover::after { + content: " (show details)"; +} + +#environment-header h2:hover::after { + content: " (hide details)"; + color: #bbb; + font-style: italic; + cursor: pointer; + font-size: 12px; +} + +#environment-header.collapsed h2:hover::after { + content: " (show details)"; + color: #bbb; + font-style: italic; + cursor: pointer; + font-size: 12px; +} + +/*------------------ + * 3. Sorting items + *------------------*/ +.sortable { + cursor: pointer; +} +.sortable.desc:after { + content: " "; + position: relative; + left: 5px; + bottom: -12.5px; + border: 10px solid #4caf50; + border-bottom: 0; + border-left-color: transparent; + border-right-color: transparent; +} +.sortable.asc:after { + content: " "; + position: relative; + left: 5px; + bottom: 12.5px; + border: 10px solid #4caf50; + border-top: 0; + border-left-color: transparent; + border-right-color: transparent; +} + +.hidden, .summary__reload__button.hidden { + display: none; +} + +.summary__data { + flex: 0 0 550px; +} +.summary__reload { + flex: 1 1; + display: flex; + justify-content: center; +} +.summary__reload__button { + flex: 0 0 300px; + display: flex; + color: white; + font-weight: bold; + background-color: #4caf50; + text-align: center; + justify-content: center; + align-items: center; + border-radius: 3px; + cursor: pointer; +} +.summary__reload__button:hover { + background-color: #46a049; +} +.summary__spacer { + flex: 0 0 550px; +} + +.controls { + display: flex; + justify-content: space-between; +} + +.filters, +.collapse { + display: flex; + align-items: center; +} +.filters button, +.collapse button { + color: #999; + border: none; + background: none; + cursor: pointer; + text-decoration: underline; +} +.filters button:hover, +.collapse button:hover { + color: #ccc; +} + +.filter__label { + margin-right: 10px; +} diff --git a/docs/docker_scout.yaml b/docs/docker_scout.yaml index 37b012fa..99acc8ba 100644 --- a/docs/docker_scout.yaml +++ b/docs/docker_scout.yaml @@ -2,14 +2,13 @@ command: docker scout short: Command line tool for Docker Scout long: Command line tool for Docker Scout usage: docker scout [command] -pname: docker -plink: docker.yaml cname: - docker scout attestation - docker scout cache - docker scout compare - docker scout config - docker scout cves + - docker scout docker-cli-plugin-hooks - docker scout enroll - docker scout environment - docker scout help @@ -19,6 +18,8 @@ cname: - docker scout quickview - docker scout recommendations - docker scout repo + - docker scout sbom + - docker scout stream - docker scout version - docker scout vex - docker scout watch @@ -28,6 +29,7 @@ clink: - docker_scout_compare.yaml - docker_scout_config.yaml - docker_scout_cves.yaml + - docker_scout_docker-cli-plugin-hooks.yaml - docker_scout_enroll.yaml - docker_scout_environment.yaml - docker_scout_help.yaml @@ -37,33 +39,13 @@ clink: - docker_scout_quickview.yaml - docker_scout_recommendations.yaml - docker_scout_repo.yaml + - docker_scout_sbom.yaml + - docker_scout_stream.yaml - docker_scout_version.yaml - docker_scout_vex.yaml - docker_scout_watch.yaml -options: - - option: debug - value_type: bool - default_value: "false" - description: Debug messages - deprecated: false - hidden: true - experimental: false - experimentalcli: false - kubernetes: false - swarm: false - - option: verbose-debug - value_type: bool - default_value: "false" - description: Verbose debug - deprecated: false - hidden: true - experimental: false - experimentalcli: false - kubernetes: false - swarm: false deprecated: false experimental: false experimentalcli: false kubernetes: false -swarm: false - +swarm: false \ No newline at end of file diff --git a/docs/docker_scout_policy.yaml b/docs/docker_scout_policy.yaml index e2772f5e..32478195 100644 --- a/docs/docker_scout_policy.yaml +++ b/docs/docker_scout_policy.yaml @@ -6,14 +6,13 @@ long: |- The image analysis is uploaded to Docker Scout where policies get evaluated. The policy evaluation results may take a few minutes to become available. -usage: docker scout policy [IMAGE | REPO] pname: docker scout plink: docker_scout.yaml cname: - docker scout policy publish clink: - docker_scout_policy_publish.yaml -options: +inherited_options: - option: exit-code shorthand: e value_type: bool @@ -113,7 +112,6 @@ options: experimentalcli: false kubernetes: false swarm: false -inherited_options: - option: debug value_type: bool default_value: "false" @@ -134,33 +132,8 @@ inherited_options: experimentalcli: false kubernetes: false swarm: false -examples: |- - ### Evaluate policies against an image and display the results - - ```console - $ docker scout policy dockerscoutpolicy/customers-api-service:0.0.1 - ``` - - ### Evaluate policies against an image for a specific organization - - ```console - $ docker scout policy dockerscoutpolicy/customers-api-service:0.0.1 --org dockerscoutpolicy - ``` - - ### Evaluate policies against an image with a specific platform - - ```console - $ docker scout policy dockerscoutpolicy/customers-api-service:0.0.1 --platform linux/amd64 - ``` - - ### Compare policy results for a repository in a specific environment - - ```console - $ docker scout policy dockerscoutpolicy/customers-api-service --to-env production - ``` deprecated: false experimental: false experimentalcli: true kubernetes: false -swarm: false - +swarm: false \ No newline at end of file diff --git a/docs/reporte.html b/docs/reporte.html new file mode 100644 index 00000000..6b4ecb4c --- /dev/null +++ b/docs/reporte.html @@ -0,0 +1,1094 @@ + + + + + reporte.html + + + + +

reporte.html

+

Report generated on 03-Aug-2026 at 20:16:24 by pytest-html + v4.2.0

+
+

Environment

+
+
+ + + + + +
+
+

Summary

+
+
+

0 test took 60 ms.

+

(Un)check the boxes to filter the results.

+
+ +
+
+
+
+ + 0 Failed, + + 0 Passed, + + 0 Skipped, + + 0 Expected failures, + + 0 Unexpected passes, + + 0 Errors, + + 0 Reruns + + 0 Retried, +
+
+  /  +
+
+
+
+
+
+
+
+ + + + + + + + + +
ResultTestDurationLinks
+
+
+ +
+ + \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..ca92d5e6 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +testpaths = tests +python_files = test_*.py +addopts = -ra --tb=short diff --git a/reporte.html b/reporte.html new file mode 100644 index 00000000..cab9eb9a --- /dev/null +++ b/reporte.html @@ -0,0 +1,1094 @@ + + + + + reporte.html + + + + +

reporte.html

+

Report generated on 03-Aug-2026 at 20:35:21 by pytest-html + v4.2.0

+
+

Environment

+
+
+ + + + + +
+
+

Summary

+
+
+

660 tests took 00:00:01.

+

(Un)check the boxes to filter the results.

+
+ +
+
+
+
+ + 0 Failed, + + 660 Passed, + + 0 Skipped, + + 0 Expected failures, + + 0 Unexpected passes, + + 0 Errors, + + 0 Reruns + + 0 Retried, +
+
+  /  +
+
+
+
+
+
+
+
+ + + + + + + + + +
ResultTestDurationLinks
+ + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..b3c4c2c8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pytest>=8.0 +pyyaml>=6.0 +pytest-html>=4.1 diff --git a/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc b/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc new file mode 100644 index 00000000..130cfcd0 Binary files /dev/null and b/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_yaml_schema.cpython-314-pytest-9.0.2.pyc b/tests/__pycache__/test_yaml_schema.cpython-314-pytest-9.0.2.pyc new file mode 100644 index 00000000..91ec9c13 Binary files /dev/null and b/tests/__pycache__/test_yaml_schema.cpython-314-pytest-9.0.2.pyc differ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..6926c692 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,106 @@ +""" +Fixtures compartidos para las pruebas de validación de los archivos +docker_scout_*.yaml del repositorio docker/scout-cli. + +Estas pruebas NO prueban el binario de la CLI (closed-source), sino los +artefactos de documentación/configuración versionados en docs/: los YAML +que describen cada comando (y que generan los .md) y su consistencia +estructural y referencial. +""" +import pathlib +import re + +import pytest +import yaml + +# Carpeta docs/ del repo. Por defecto apunta a la carpeta de ejemplo +# incluida en este proyecto. Para correr las pruebas contra el repo +# real, exporta la variable de entorno SCOUT_DOCS_DIR, por ejemplo: +# +# export SCOUT_DOCS_DIR=/ruta/a/scout-cli/docs +# pytest +# +import os + +DEFAULT_DOCS_DIR = pathlib.Path(__file__).parent.parent / "docs" +DOCS_DIR = pathlib.Path(os.environ.get("SCOUT_DOCS_DIR", DEFAULT_DOCS_DIR)) + +YAML_FILENAME_RE = re.compile(r"^docker_scout(_[a-z0-9-]+)*\.yaml$") + +# value_type conocidos usados por los comandos de docker scout +KNOWN_VALUE_TYPES = { + "bool", + "string", + "stringSlice", + "stringArray", + "int", + "int64", + "float32", + "float64", + "duration", +} + +REQUIRED_TOP_LEVEL_FIELDS = { + "command", + "short", + "long", + "deprecated", + "experimental", + "experimentalcli", + "kubernetes", + "swarm", +} + +REQUIRED_OPTION_FIELDS = { + "option", + "value_type", + "description", + "deprecated", + "hidden", + "experimental", + "experimentalcli", + "kubernetes", + "swarm", +} + +BOOL_FIELDS = {"deprecated", "experimental", "experimentalcli", "kubernetes", "swarm"} + + +def discover_yaml_files(): + if not DOCS_DIR.exists(): + return [] + return sorted(DOCS_DIR.glob("docker_scout*.yaml")) + + +@pytest.fixture(scope="session") +def docs_dir(): + return DOCS_DIR + + +@pytest.fixture(scope="session") +def all_yaml_paths(): + paths = discover_yaml_files() + if not paths: + pytest.fail( + f"No se encontraron archivos docker_scout*.yaml en {DOCS_DIR}. " + "Define SCOUT_DOCS_DIR apuntando a la carpeta docs/ del repo clonado." + ) + return paths + + +@pytest.fixture(scope="session") +def loaded_docs(all_yaml_paths): + """dict: {path -> contenido parseado del yaml}""" + docs = {} + for path in all_yaml_paths: + with open(path, "r", encoding="utf-8") as fh: + docs[path] = yaml.safe_load(fh) + return docs + + +def pytest_generate_tests(metafunc): + """Parametriza automáticamente cualquier test que pida 'yaml_path'.""" + if "yaml_path" in metafunc.fixturenames: + paths = discover_yaml_files() + ids = [p.name for p in paths] + metafunc.parametrize("yaml_path", paths, ids=ids) diff --git a/tests/test_yaml_schema.py b/tests/test_yaml_schema.py new file mode 100644 index 00000000..2b0036e8 --- /dev/null +++ b/tests/test_yaml_schema.py @@ -0,0 +1,248 @@ +""" +Pruebas unitarias de los archivos docker_scout_*.yaml (docs/) del +repositorio docker/scout-cli. + +Qué se valida: + 1. Sintaxis: cada YAML parsea correctamente y produce un dict. + 2. Esquema base: campos obligatorios presentes y con el tipo correcto. + 3. Comandos "hoja": usage/options bien formados. + 4. Comandos "padre": cname/clink consistentes entre sí. + 5. Referencias cruzadas: plink/pname apuntan a un archivo/comando real + y esa relación es recíproca (el padre lista al hijo y viceversa). + 6. Documentación: existe un .md correspondiente a cada .yaml. +""" +import yaml + +from conftest import ( + BOOL_FIELDS, + KNOWN_VALUE_TYPES, + REQUIRED_OPTION_FIELDS, + REQUIRED_TOP_LEVEL_FIELDS, + YAML_FILENAME_RE, +) + + +# --------------------------------------------------------------------------- +# 1. Sintaxis +# --------------------------------------------------------------------------- + +def test_filename_matches_naming_convention(yaml_path): + assert YAML_FILENAME_RE.match(yaml_path.name), ( + f"'{yaml_path.name}' no sigue la convención docker_scout(_subcomando)*.yaml" + ) + + +def test_yaml_parses_and_is_a_mapping(yaml_path): + with open(yaml_path, "r", encoding="utf-8") as fh: + content = yaml.safe_load(fh) + assert isinstance(content, dict), f"{yaml_path.name} no produce un mapeo YAML válido" + + +# --------------------------------------------------------------------------- +# 2. Esquema base (todos los archivos) +# --------------------------------------------------------------------------- + +def test_required_top_level_fields_present(yaml_path, loaded_docs): + doc = loaded_docs[yaml_path] + missing = REQUIRED_TOP_LEVEL_FIELDS - doc.keys() + assert not missing, f"{yaml_path.name}: faltan campos obligatorios {missing}" + + +def test_boolean_fields_have_bool_type(yaml_path, loaded_docs): + doc = loaded_docs[yaml_path] + for field in BOOL_FIELDS: + if field in doc: + assert isinstance(doc[field], bool), ( + f"{yaml_path.name}: el campo '{field}' debería ser booleano, " + f"se encontró {type(doc[field]).__name__}" + ) + + +def test_command_field_starts_with_docker_scout(yaml_path, loaded_docs): + doc = loaded_docs[yaml_path] + assert doc["command"].startswith("docker scout"), ( + f"{yaml_path.name}: 'command' debería empezar con 'docker scout', " + f"tiene '{doc['command']}'" + ) + + +def test_short_and_long_are_non_empty_strings(yaml_path, loaded_docs): + doc = loaded_docs[yaml_path] + for field in ("short", "long"): + value = doc.get(field, "") + assert isinstance(value, str) and value.strip(), ( + f"{yaml_path.name}: '{field}' debe ser texto no vacío" + ) + + +def test_command_name_matches_filename(yaml_path, loaded_docs): + """docker scout integration list -> docker_scout_integration_list.yaml""" + doc = loaded_docs[yaml_path] + expected_filename = doc["command"].replace(" ", "_") + ".yaml" + assert yaml_path.name == expected_filename, ( + f"El nombre de archivo '{yaml_path.name}' no coincide con el comando " + f"'{doc['command']}' (se esperaba '{expected_filename}')" + ) + + +# --------------------------------------------------------------------------- +# 3. Comandos "hoja" (los que tienen 'usage' / 'options' propias) +# --------------------------------------------------------------------------- + +def _validate_option_entry(entry, context): + missing = REQUIRED_OPTION_FIELDS - entry.keys() + assert not missing, f"{context}: opción incompleta, faltan {missing}: {entry}" + assert entry["value_type"] in KNOWN_VALUE_TYPES, ( + f"{context}: value_type desconocido '{entry['value_type']}' en opción " + f"'{entry['option']}'" + ) + if "shorthand" in entry and entry["shorthand"] is not None: + assert len(entry["shorthand"]) == 1, ( + f"{context}: el shorthand de '{entry['option']}' debe ser un solo caracter" + ) + for field in ("deprecated", "hidden", "experimental", "experimentalcli", "kubernetes", "swarm"): + assert isinstance(entry[field], bool), ( + f"{context}: el campo '{field}' de la opción '{entry['option']}' debe ser booleano" + ) + + +def test_usage_present_when_command_is_leaf(yaml_path, loaded_docs): + doc = loaded_docs[yaml_path] + if "options" in doc: + assert "usage" in doc and doc["usage"].strip(), ( + f"{yaml_path.name}: tiene 'options' pero no define 'usage'" + ) + assert doc["usage"].startswith(doc["command"]), ( + f"{yaml_path.name}: 'usage' debería iniciar con el propio comando" + ) + + +def test_options_are_well_formed(yaml_path, loaded_docs): + doc = loaded_docs[yaml_path] + for entry in doc.get("options", []): + _validate_option_entry(entry, f"{yaml_path.name} [options]") + + +def test_inherited_options_are_well_formed(yaml_path, loaded_docs): + doc = loaded_docs[yaml_path] + for entry in doc.get("inherited_options", []): + _validate_option_entry(entry, f"{yaml_path.name} [inherited_options]") + + +def test_option_names_are_unique_within_file(yaml_path, loaded_docs): + doc = loaded_docs[yaml_path] + names = [e["option"] for e in doc.get("options", [])] + duplicates = {n for n in names if names.count(n) > 1} + assert not duplicates, f"{yaml_path.name}: opciones duplicadas en 'options': {duplicates}" + + +def test_options_and_inherited_options_do_not_collide(yaml_path, loaded_docs): + doc = loaded_docs[yaml_path] + own = {e["option"] for e in doc.get("options", [])} + inherited = {e["option"] for e in doc.get("inherited_options", [])} + overlap = own & inherited + assert not overlap, ( + f"{yaml_path.name}: las opciones {overlap} están tanto en 'options' " + "como en 'inherited_options'" + ) + + +# --------------------------------------------------------------------------- +# 4. Comandos "padre" (los que tienen 'cname' / 'clink') +# --------------------------------------------------------------------------- + +def test_cname_and_clink_have_same_length(yaml_path, loaded_docs): + doc = loaded_docs[yaml_path] + if "cname" in doc or "clink" in doc: + assert "cname" in doc and "clink" in doc, ( + f"{yaml_path.name}: debe tener tanto 'cname' como 'clink' o ninguno" + ) + assert len(doc["cname"]) == len(doc["clink"]), ( + f"{yaml_path.name}: 'cname' ({len(doc['cname'])} items) y 'clink' " + f"({len(doc['clink'])} items) tienen longitudes distintas" + ) + + +def test_clink_filenames_match_cname_entries(yaml_path, loaded_docs): + doc = loaded_docs[yaml_path] + for cname, clink in zip(doc.get("cname", []), doc.get("clink", [])): + expected = cname.replace(" ", "_") + ".yaml" + assert clink == expected, ( + f"{yaml_path.name}: cname '{cname}' debería enlazar a '{expected}', " + f"pero clink dice '{clink}'" + ) + + +def test_clink_files_exist(yaml_path, loaded_docs, docs_dir): + doc = loaded_docs[yaml_path] + for clink in doc.get("clink", []): + target = docs_dir / clink + assert target.exists(), f"{yaml_path.name}: clink '{clink}' no existe en {docs_dir}" + + +def test_a_leaf_command_has_no_children(yaml_path, loaded_docs): + """Un comando no debería mezclar 'options' propias con 'cname' (hoja vs. padre).""" + doc = loaded_docs[yaml_path] + if "options" in doc: + assert "cname" not in doc, ( + f"{yaml_path.name}: tiene 'options' y 'cname' a la vez; se esperaba " + "que fuera un comando hoja o un comando padre, no ambos" + ) + + +# --------------------------------------------------------------------------- +# 5. Referencias cruzadas plink/pname (relación bidireccional padre-hijo) +# --------------------------------------------------------------------------- + +def test_plink_file_exists(yaml_path, loaded_docs, docs_dir): + doc = loaded_docs[yaml_path] + if "plink" in doc: + target = docs_dir / doc["plink"] + assert target.exists(), f"{yaml_path.name}: plink '{doc['plink']}' no existe en {docs_dir}" + + +def test_pname_and_plink_are_consistent(yaml_path, loaded_docs): + doc = loaded_docs[yaml_path] + if "pname" in doc or "plink" in doc: + assert "pname" in doc and "plink" in doc, ( + f"{yaml_path.name}: debe tener tanto 'pname' como 'plink' o ninguno" + ) + expected_plink = doc["pname"].replace(" ", "_") + ".yaml" + assert doc["plink"] == expected_plink, ( + f"{yaml_path.name}: pname '{doc['pname']}' debería enlazar a " + f"'{expected_plink}', pero plink dice '{doc['plink']}'" + ) + + +def test_parent_actually_lists_this_command_as_child(yaml_path, loaded_docs, docs_dir): + """Si A dice que su padre es B, entonces B debe listar a A en su cname/clink.""" + doc = loaded_docs[yaml_path] + if "plink" not in doc: + return # es la raíz (docker_scout.yaml), no tiene padre + parent_path = docs_dir / doc["plink"] + with open(parent_path, "r", encoding="utf-8") as fh: + parent_doc = yaml.safe_load(fh) + assert doc["command"] in parent_doc.get("cname", []), ( + f"{yaml_path.name}: el padre '{doc['plink']}' no lista a " + f"'{doc['command']}' en su 'cname'" + ) + assert yaml_path.name in parent_doc.get("clink", []), ( + f"{yaml_path.name}: el padre '{doc['plink']}' no lista '{yaml_path.name}' " + "en su 'clink'" + ) + + +# --------------------------------------------------------------------------- +# 6. Documentación asociada (.md) +# --------------------------------------------------------------------------- + +def test_corresponding_md_file_exists(yaml_path, docs_dir): + # docker_scout_cves.yaml -> scout_cves.md ; docker_scout.yaml -> scout.md + md_name = yaml_path.stem.replace("docker_scout", "scout", 1) + ".md" + if md_name == ".md": + md_name = "scout.md" + md_path = docs_dir / md_name + assert md_path.exists(), ( + f"{yaml_path.name}: no se encontró la documentación correspondiente " + f"'{md_name}' en {docs_dir}" + )