Repository: nginx/kubernetes-ingress

Issue:
Add python script for nginx modules
The python script for agent & otel should ideally be done on all image types to ensure we don't miss any modules on certain images.  Design a new test job to validate images and modules. e.g.

```
{
  [
    {
       "image": "debian",
       "modules":
         - nginx-module-njs
         - nginx-module-otel
         - nginx-module-image-filter
         - nginx-module-xslt
    },
    {
       "image": "debian-plus",
       "modules":
         - nginx-plus-module-njs
         - nginx-plus-module-otel
         - nginx-plus-module-fips-check
    },
    {
       "image": "debian-plus-nap",
       "modules":
         - nginx-plus-module-njs
         - nginx-plus-module-otel
         - nginx-plus-module-fips-check
         - nginx-plus-module-appprotect
         - nginx-agent
    },
  ]
}
```


Discussion:

Solution patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c438c0b1f..6265b3155 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -377,6 +377,66 @@ jobs:
     secrets: inherit
     if: ${{ inputs.force || (needs.checks.outputs.forked_workflow == 'true' && needs.checks.outputs.docs_only == 'false') || (needs.checks.outputs.forked_workflow == 'false' && needs.checks.outputs.stable_image_exists != 'true' && needs.checks.outputs.docs_only == 'false') }}
 
+  package-tests:
+    if: ${{ needs.checks.outputs.docs_only != 'true' && (inputs.run_tests && inputs.run_tests || true) }}
+    name: Package Tests
+    runs-on: ubuntu-22.04
+    needs: [checks, binaries, build-docker, build-docker-plus, build-docker-nap]
+    permissions:
+      contents: read
+      pull-requests: write # for package report
+      id-token: write
+    steps:
+      - name: Checkout Repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+      - name: Setup QEMU
+        uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
+        with:
+          platforms: arm64
+        if: ${{ needs.checks.outputs.forked_workflow == 'false' || needs.checks.outputs.docs_only == 'false' }}
+
+      - name: Authenticate to Google Cloud
+        id: auth
+        uses: google-github-actions/auth@ba79af03959ebeac9769e648f473a284504d9193 # v2.1.10
+        with:
+          token_format: access_token
+          workload_identity_provider: ${{ secrets.GCR_WORKLOAD_IDENTITY }}
+          service_account: ${{ secrets.GCR_SERVICE_ACCOUNT }}
+        if: ${{ needs.checks.outputs.forked_workflow == 'false' || needs.checks.outputs.docs_only == 'false' }}
+
+      - name: Login to GCR
+        uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
+        with:
+          registry: gcr.io
+          username: oauth2accesstoken
+          password: ${{ steps.auth.outputs.access_token }}
+        if: ${{ needs.checks.outputs.forked_workflow == 'false' || needs.checks.outputs.docs_only == 'false' }}
+
+      - name: Install Python dependencies
+        run: |
+          make -f tests/Makefile setup-venv
+        if: ${{ needs.checks.outputs.forked_workflow == 'false' || needs.checks.outputs.docs_only == 'false' }}
+
+      - name: Run tests
+        id: packages
+        run: |
+          source tests/venv/bin/activate
+          python tests/scripts/check_container_packages.py --tag ${{ needs.checks.outputs.build_tag }} --log package_output.txt
+        if: ${{ needs.checks.outputs.forked_workflow == 'false' || needs.checks.outputs.docs_only == 'false' }}
+
+      - name: Add comment
+        run: |
+          # make sure the comment is formatted correctly, as a code block
+          echo '### Package Report' > output.txt
+          echo '```' >> output.txt
+          cat package_output.txt >> output.txt
+          echo '```' >> output.txt
+          gh pr comment --edit-last --create-if-none ${{ github.event.pull_request.number }} -F output.txt
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        if: ${{ ( needs.checks.outputs.forked_workflow == 'false' || needs.checks.outputs.docs_only == 'false' ) && github.event.pull_request }}
+
   helm-tests:
     if: ${{ needs.checks.outputs.docs_only != 'true' && (inputs.run_tests && inputs.run_tests || true) }}
     name: Helm Tests ${{ matrix.base-os }}
@@ -677,10 +737,12 @@ jobs:
     if: ${{ !cancelled() }}
     runs-on: ubuntu-22.04
     name: Final CI Results
-    needs: [tag-stable, build-docker, build-docker-plus, build-docker-nap, smoke-tests-oss, smoke-tests-plus, smoke-tests-nap]
+    needs: [tag-stable, build-docker, build-docker-plus, build-docker-nap, smoke-tests-oss, smoke-tests-plus, smoke-tests-nap, package-tests, helm-tests]
     steps:
       - run: |
           tagResult="${{ needs.tag-stable.result }}"
+          packageResult="${{ needs.package-tests.result }}"
+          helmResult="${{ needs.helm-tests.result }}"
           smokeOSSResult="${{ needs.smoke-tests-oss.result }}"
           smokePlusResult="${{ needs.smoke-tests-plus.result }}"
           smokeNAPResult="${{ needs.smoke-tests-nap.result }}"
@@ -708,6 +770,12 @@ jobs:
           if [[ $buildNAPResult != "success" && $buildNAPResult != "skipped" ]]; then
             exit 1
           fi
+          if [[ $helmResult != "success" && $helmResult != "skipped" ]]; then
+            exit 1
+          fi
+          if [[ $packageResult != "success" && $packageResult != "skipped" ]]; then
+            exit 1
+          fi
 
   trigger-image-promotion:
     name: Promote images on Force Run
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 82d1981cf..4bf48df7b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -27,7 +27,7 @@ repos:
         args: [--fix=lf]
       - id: name-tests-test
         args: [--pytest-test-first]
-        exclude: ^(tests/suite/utils|tests/suite/fixtures|tests/suite/grpc|tests/settings.py)
+        exclude: ^(tests/suite/utils|tests/suite/fixtures|tests/suite/grpc|tests/settings.py|tests/scripts)
       - id: no-commit-to-branch
       - id: requirements-txt-fixer
       - id: fix-byte-order-marker



Test patch:
diff --git a/tests/Makefile b/tests/Makefile
index 8fe5f9603..80a9ac807 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -56,7 +56,7 @@ $(MINIKUBE_KUBE_CONFIG_FOLDER): $(KUBE_CONFIG_FOLDER)
 setup-venv:
 	$(PYTHON) -m venv $(ROOT_DIR)/tests/venv;
 	source $(ROOT_DIR)/tests/venv/bin/activate \
-	&& pip install --require-hashes -r requirements.txt --no-deps \
+	&& pip install --require-hashes -r $(ROOT_DIR)/tests/requirements.txt --no-deps \
 	&& playwright install --with-deps chromium
 
 
diff --git a/tests/data/modules/data.json b/tests/data/modules/data.json
new file mode 100644
index 000000000..b30eb79c7
--- /dev/null
+++ b/tests/data/modules/data.json
@@ -0,0 +1,737 @@
+{
+  "images": [
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic/nginx-ingress",
+      "tag_suffix": "",
+      "packages": [
+        {
+          "name": "nginx",
+          "version": "1.27.5"
+        },
+        {
+          "name": "nginx-module-njs",
+          "version": "1.27.5"
+        },
+        {
+          "name": "nginx-module-otel",
+          "version": "1.27.5"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "3.0"
+        }
+      ],
+      "system": "debian",
+      "platforms": [
+        "linux/amd64",
+        "linux/arm64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic/nginx-plus-ingress",
+      "tag_suffix": "",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "3.0"
+        }
+      ],
+      "system": "debian",
+      "platforms": [
+        "linux/amd64",
+        "linux/arm64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic-nap/nginx-plus-ingress",
+      "tag_suffix": "",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-appprotect",
+          "version": "34+5.442"
+        },
+        {
+          "name": "app-protect",
+          "version": "34+5.442"
+        },
+        {
+          "name": "app-protect-attack-signatures",
+          "version": "2025"
+        },
+        {
+          "name": "app-protect-threat-campaigns",
+          "version": "2025"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "2"
+        }
+      ],
+      "system": "debian",
+      "platforms": [
+        "linux/amd64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic-nap-v5/nginx-plus-ingress",
+      "tag_suffix": "",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-appprotect",
+          "version": "34+5.442"
+        },
+        {
+          "name": "app-protect-module-plus",
+          "version": "34+5.442"
+        },
+        {
+          "name": "app-protect-plugin",
+          "version": "6.16.0"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "2"
+        }
+      ],
+      "system": "debian",
+      "platforms": [
+        "linux/amd64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic-dos/nginx-plus-ingress",
+      "tag_suffix": "",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-appprotectdos",
+          "version": "34+4"
+        },
+        {
+          "name": "app-protect-dos",
+          "version": "34+4"
+        }
+      ],
+      "system": "debian",
+      "platforms": [
+        "linux/amd64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic-dos-nap/nginx-plus-ingress",
+      "tag_suffix": "",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-appprotect",
+          "version": "34+5.442"
+        },
+        {
+          "name": "app-protect",
+          "version": "34+5.442"
+        },
+        {
+          "name": "app-protect-attack-signatures",
+          "version": "2025"
+        },
+        {
+          "name": "app-protect-threat-campaigns",
+          "version": "2025"
+        },
+        {
+          "name": "nginx-plus-module-appprotectdos",
+          "version": "34+4"
+        },
+        {
+          "name": "app-protect-dos",
+          "version": "34+4"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "2"
+        }
+      ],
+      "system": "debian",
+      "platforms": [
+        "linux/amd64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic/nginx-ingress",
+      "tag_suffix": "-alpine",
+      "packages": [
+        {
+          "name": "nginx",
+          "version": "1.27.5"
+        },
+        {
+          "name": "nginx-module-njs",
+          "version": "1.27.5"
+        },
+        {
+          "name": "nginx-module-otel",
+          "version": "1.27.5"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "3.0"
+        }
+      ],
+      "system": "alpine",
+      "platforms": [
+        "linux/amd64",
+        "linux/arm64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic/nginx-plus-ingress",
+      "tag_suffix": "-alpine",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-r2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "3.0"
+        }
+      ],
+      "system": "alpine",
+      "platforms": [
+        "linux/amd64",
+        "linux/arm64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic/nginx-plus-ingress",
+      "tag_suffix": "-alpine-fips",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-r2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "3.0"
+        }
+      ],
+      "system": "alpine",
+      "platforms": [
+        "linux/amd64",
+        "linux/arm64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic-nap/nginx-plus-ingress",
+      "tag_suffix": "-alpine-fips",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-r2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "2"
+        },
+        {
+          "name": "nginx-plus-module-appprotect",
+          "version": "34.5.442"
+        },
+        {
+          "name": "app-protect",
+          "version": "34.5.442"
+        },
+        {
+          "name": "app-protect-attack-signatures",
+          "version": "2025"
+        },
+        {
+          "name": "app-protect-threat-campaigns",
+          "version": "2025"
+        }
+      ],
+      "system": "alpine",
+      "platforms": [
+        "linux/amd64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic-nap-v5/nginx-plus-ingress",
+      "tag_suffix": "-alpine-fips",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-r2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "2"
+        },
+        {
+          "name": "nginx-plus-module-appprotect",
+          "version": "34.5.442"
+        },
+        {
+          "name": "app-protect-module-plus",
+          "version": "34.5.442"
+        },
+        {
+          "name": "app-protect-plugin",
+          "version": "6.16.0"
+        }
+      ],
+      "system": "alpine",
+      "platforms": [
+        "linux/amd64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic/nginx-ingress",
+      "tag_suffix": "-ubi",
+      "packages": [
+        {
+          "name": "nginx",
+          "version": "1.27.5"
+        },
+        {
+          "name": "nginx-module-njs",
+          "version": "1.27.5"
+        },
+        {
+          "name": "nginx-module-otel",
+          "version": "1.27.5"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "3.0"
+        }
+      ],
+      "system": "ubi",
+      "platforms": [
+        "linux/amd64",
+        "linux/arm64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic/nginx-plus-ingress",
+      "tag_suffix": "-ubi",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "3.0"
+        }
+      ],
+      "system": "ubi",
+      "platforms": [
+        "linux/amd64",
+        "linux/arm64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic-nap/nginx-plus-ingress",
+      "tag_suffix": "-ubi",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "2"
+        },
+        {
+          "name": "nginx-plus-module-appprotect",
+          "version": "34+5.442"
+        },
+        {
+          "name": "app-protect",
+          "version": "34+5.442"
+        },
+        {
+          "name": "app-protect-attack-signatures",
+          "version": "2025"
+        },
+        {
+          "name": "app-protect-threat-campaigns",
+          "version": "2025"
+        }
+      ],
+      "system": "ubi",
+      "platforms": [
+        "linux/amd64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic-nap-v5/nginx-plus-ingress",
+      "tag_suffix": "-ubi",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "2"
+        },
+        {
+          "name": "nginx-plus-module-appprotect",
+          "version": "34+5.442"
+        },
+        {
+          "name": "app-protect-module-plus",
+          "version": "34+5.442"
+        },
+        {
+          "name": "app-protect-plugin",
+          "version": "6.16.0"
+        }
+      ],
+      "system": "ubi",
+      "platforms": [
+        "linux/amd64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic-nap/nginx-plus-ingress",
+      "tag_suffix": "-ubi8",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "2"
+        },
+        {
+          "name": "nginx-plus-module-appprotect",
+          "version": "34+5.442"
+        },
+        {
+          "name": "app-protect",
+          "version": "34+5.442"
+        },
+        {
+          "name": "app-protect-attack-signatures",
+          "version": "2025"
+        },
+        {
+          "name": "app-protect-threat-campaigns",
+          "version": "2025"
+        }
+      ],
+      "system": "ubi",
+      "platforms": [
+        "linux/amd64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic-nap-v5/nginx-plus-ingress",
+      "tag_suffix": "-ubi8",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "2"
+        },
+        {
+          "name": "nginx-plus-module-appprotect",
+          "version": "34+5.442"
+        },
+        {
+          "name": "app-protect-module-plus",
+          "version": "34+5.442"
+        },
+        {
+          "name": "app-protect-plugin",
+          "version": "6.16.0"
+        }
+      ],
+      "system": "ubi",
+      "platforms": [
+        "linux/amd64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic-dos/nginx-plus-ingress",
+      "tag_suffix": "-ubi",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-appprotectdos",
+          "version": "34+4"
+        },
+        {
+          "name": "app-protect-dos",
+          "version": "34+4"
+        }
+      ],
+      "system": "ubi",
+      "platforms": [
+        "linux/amd64"
+      ]
+    },
+    {
+      "image": "gcr.io/f5-gcs-7899-ptg-ingrss-ctlr/dev/nginx-ic-dos-nap/nginx-plus-ingress",
+      "tag_suffix": "-ubi",
+      "packages": [
+        {
+          "name": "nginx-plus",
+          "version": "34-2"
+        },
+        {
+          "name": "nginx-plus-module-njs",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-otel",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-fips-check",
+          "version": "34"
+        },
+        {
+          "name": "nginx-plus-module-appprotect",
+          "version": "34+5.442"
+        },
+        {
+          "name": "nginx-plus-module-appprotectdos",
+          "version": "34+4"
+        },
+        {
+          "name": "nginx-agent",
+          "version": "2"
+        },
+        {
+          "name": "app-protect",
+          "version": "34+5.442"
+        },
+        {
+          "name": "app-protect-attack-signatures",
+          "version": "2025"
+        },
+        {
+          "name": "app-protect-threat-campaigns",
+          "version": "2025"
+        },
+        {
+          "name": "app-protect-dos",
+          "version": "34+4"
+        }
+      ],
+      "system": "ubi",
+      "platforms": [
+        "linux/amd64"
+      ]
+    }
+  ]
+}
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 9f6bf5504..2137354f4 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -229,6 +229,10 @@ cryptography==45.0.5 \
     # via
     #   -r requirements.in
     #   pyopenssl
+docker==7.1.0 \
+    --hash=sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c \
+    --hash=sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0
+    # via -r requirements.in
 durationpy==0.10 \
     --hash=sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba \
     --hash=sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286
@@ -689,6 +693,7 @@ requests==2.32.4 \
     --hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422
     # via
     #   -r requirements.in
+    #   docker
     #   forcediphttpsadapter
     #   kubernetes
     #   requests-oauthlib
@@ -722,6 +727,7 @@ urllib3==2.5.0 \
     --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc
     # via
     #   -r requirements.in
+    #   docker
     #   kubernetes
     #   requests
 websocket-client==1.8.0 \
diff --git a/tests/scripts/check_container_packages.py b/tests/scripts/check_container_packages.py
new file mode 100755
index 000000000..f4010da4f
--- /dev/null
+++ b/tests/scripts/check_container_packages.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+
+import argparse
+import json
+import logging
+import os
+import re
+
+import docker
+import docker.errors
+
+# parse args
+parser = argparse.ArgumentParser()
+parser.add_argument("-t", "--tag", type=str, help="NGINX Ingress Controller image tag", default="edge")
+parser.add_argument("-l", "--log", type=str, help="log file", required=False)
+args = parser.parse_args()
+
+# Create a logger
+logger = logging.getLogger("package_checker")
+logger.setLevel(logging.DEBUG)
+
+# Create a stream handler (for stdout)
+stream_handler = logging.StreamHandler()
+stream_handler.setLevel(logging.DEBUG)
+stream_handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
+logger.addHandler(stream_handler)
+
+if args.log:
+    # Create a file handler
+    file_handler = logging.FileHandler(args.log)
+    file_handler.setLevel(logging.INFO)
+    file_handler.setFormatter(logging.Formatter("%(message)s"))
+    logger.addHandler(file_handler)
+
+systems = {
+    "alpine": {
+        "cmd": "apk list",
+        "regex": "^(.+?)-(\\d+.+?)\\s+(\\w+).*\\[installed\\]",
+    },
+    "debian": {
+        "cmd": "dpkg -l",
+        "regex": "ii\\s+(.+?)\\s+(.+?)\\s+(\\w+?)\\s",
+    },
+    "ubi": {
+        "cmd": "rpm -q",
+        "regex": "(.+?)-(\\d+.+)(?:\\.ngx)?\\.(\\w+)",
+    },
+}
+
+client = docker.from_env()
+script_dir = os.path.dirname(os.path.abspath(__file__))
+with open(f"{script_dir}/../data/modules/data.json") as file:
+    images = json.load(file)
+
+    for image in images["images"]:
+        for platform in image["platforms"]:
+            image_name = image["image"]
+            regex = systems[image["system"]]["regex"]
+            cmd = systems[image["system"]]["cmd"]
+            tag = f"{args.tag}{image['tag_suffix']}"
+            try:
+                i = client.images.get(f"{image['image']}:{tag}")
+                ## check if the image is for the correct platform
+                if platform != i.attrs["Os"]:
+                    raise docker.errors.ImageNotFound(
+                        f"Image {image['image']}:{tag} is not for platform {platform}, found {i.attrs['Os']}"
+                    )
+            except docker.errors.ImageNotFound:
+                ## pull the image
+                logger.debug(f"Pulling image {image['image']}:{tag} for platform {platform}")
+                i = client.images.pull(repository=image_name, tag=tag, platform=platform)
+                logger.debug(f"Image {i.id} pulled successfully")
+            for package in image["packages"]:
+                command = f"{cmd} {package['name']}"
+                output = ""
+                try:
+                    output = client.containers.run(
+                        f"{image['image']}:{tag}",
+                        command,
+                        entrypoint="",
+                        platform=platform,
+                        auto_remove=True,
+                        detach=False,
+                    )
+                except (docker.errors.ContainerError, docker.errors.NotFound) as e:
+                    logger.error(f"{e}, retrying")
+                    output = client.containers.run(
+                        f"{image['image']}:{tag}",
+                        command,
+                        entrypoint="",
+                        platform=platform,
+                        auto_remove=True,
+                        detach=False,
+                    )
+                result = re.search(regex, output.decode("utf-8").strip())
+                assert result, logger.error(
+                    f"{package['name']} not found in {image['image']}, output: {output.decode('utf-8').strip()}"
+                )
+                assert result.group(2).startswith(package["version"]), logger.error(
+                    f"{package['name']} version {package['version']} does not match {result.group(2)}"
+                )
+                logger.info(f"{image_name}:{tag}, {result.group(1)}, {result.group(2)}, {result.group(3)}")