diff --git a/konflux/README.md b/konflux/README.md new file mode 100644 index 00000000..bbe7e325 --- /dev/null +++ b/konflux/README.md @@ -0,0 +1,175 @@ +# Konflux onboarding and reusable TLS scan pipeline + +This directory holds Konflux resources for: + +1. **Onboarding `tls-scanner` itself** so Konflux builds the image and can run + integration checks on PRs to this repo. +2. **Publishing a reusable compliance-scan Pipeline** that layered products can + point at from their own `IntegrationTestScenario` (via Tekton git resolver) + to scan their ephemeral test environments. + +## Important: what is and is not auto-applied from git + +**None of the `Application` / `Component` / `IntegrationTestScenario` CRs are +watched or auto-applied by Konflux.** Someone with `oc`/`kubectl` access to the +Konflux tenant namespace must apply them explicitly. + +Only Pipeline / PipelineRun YAML referenced by an already-applied ITS +`resolverRef` (or by Pipelines-as-Code `.tekton/*.yaml` files) is fetched from +git at run time. + +## Layout + +| File | Applied to cluster? | Purpose | +|---|---|---| +| [`application.yaml`](application.yaml) | Yes | Konflux `Application` for this component. | +| [`component.yaml`](component.yaml) | Yes | `Component` + `ImageRepository`. Triggers Konflux's PaC bot to open a *separate* onboarding PR with generated `.tekton/` build PipelineRuns — do not hand-write those. Uses `Dockerfile.local` (public bases) so Konflux can build without `registry.ci.openshift.org`. | +| [`integration-test-scenario.yaml`](integration-test-scenario.yaml) | Yes | ITS CRs for this repo's own self-test and smoke checks. | +| [`pipelines/tls-scanner-smoke-test.yaml`](pipelines/tls-scanner-smoke-test.yaml) | No (git-resolved) | Light check: `tls-scanner --version` / `--help` on the Snapshot image. | +| [`pipelines/tls-scanner-self-test.yaml`](pipelines/tls-scanner-self-test.yaml) | No (git-resolved) | End-to-end self-test: smoke + TLS fixture pod + Job-based scan in the PipelineRun namespace. | +| [`pipelines/tls-scanner-compliance-scan.yaml`](pipelines/tls-scanner-compliance-scan.yaml) | No (git-resolved) | **Reusable pipeline for layered products.** Deploys the scanner Job into a target cluster identified by a kubeconfig Secret. | + +## Prerequisites + +This repo's Konflux tenant namespace is **`ocp-isc-tenant`**. Manifests under +`konflux/*.yaml` already use that value. + +Also required: + +- Konflux GitHub App installed on the `openshift` org (or at least this repo). +- Push access for branches matching `konflux-*` (PaC onboarding / MintMaker). + +## Onboarding tls-scanner (apply order) + +1. `oc apply -f konflux/application.yaml` (namespace: `ocp-isc-tenant`) +2. `oc apply -f konflux/component.yaml` — Konflux opens a separate PaC PR with + generated `.tekton/*.yaml` build pipelines. Review and merge **that** PR + before expecting builds. +3. After a build Snapshot exists: `oc apply -f konflux/integration-test-scenario.yaml` + +### Validating pipelines from a feature PR + +1. Temporarily set each ITS `resolverRef` `revision` param to your branch name + (e.g. `feat/konflux-tls-scan`). +2. Re-apply the ITS: `oc apply -f konflux/integration-test-scenario.yaml` +3. Open/update the GitHub PR — Konflux posts integration checks (e.g. + `tls-scanner-self-test`). Click **Details** to open the Tekton UI. +4. Because the ITS carries `test.appstudio.openshift.io/optional: "true"`, a + failing check will not block the Snapshot while bootstrapping. +5. Flip `revision` back to `main`, re-apply, and merge the PR. + +## Using tls-scanner from a layered product + +tls-scanner must run **inside** the cluster it scans (it execs into pods and +opens TCP connections to pod IPs). Layered products that already provision an +ephemeral test environment for their Snapshot should: + +1. Store a kubeconfig for that environment in a Secret in their Konflux tenant + namespace (key defaults to `kubeconfig`). +2. Apply an `IntegrationTestScenario` in **their** tenant that points at this + repo's compliance pipeline: + +```yaml +apiVersion: appstudio.redhat.com/v1beta2 +kind: IntegrationTestScenario +metadata: + name: tls-compliance-scan + namespace: + labels: + # Start optional; flip to "false" once the product is green. + test.appstudio.openshift.io/optional: "true" +spec: + application: + contexts: + - description: TLS compliance scan of the ephemeral test environment + name: application + resolverRef: + resolver: git + params: + - name: url + value: https://github.com/openshift/tls-scanner.git + # Pin to a tag or known-good commit — avoid tracking main unless intentional. + - name: revision + value: main + - name: pathInRepo + value: konflux/pipelines/tls-scanner-compliance-scan.yaml + params: + - name: TARGET_KUBECONFIG_SECRET + value: + # Optional overrides: + # - name: TARGET_KUBECONFIG_SECRET_KEY + # value: kubeconfig + # - name: SCANNER_IMAGE + # value: quay.io//tls-scanner: + # - name: NAMESPACE_FILTER + # value: my-product-ns + # - name: PQC_CHECK + # value: "true" + # - name: TLS_PROFILE_TYPE + # value: Modern + # - name: SCAN_NAMESPACE + # value: tls-scanner + # - name: LIMIT_IPS + # value: "0" +``` + +### Compliance pipeline parameters + +| Param | Required | Default | Meaning | +|---|---|---|---| +| `SNAPSHOT` | injected | — | Konflux Snapshot JSON. | +| `TARGET_KUBECONFIG_SECRET` | yes | — | Secret name in the PipelineRun namespace with the target cluster kubeconfig. | +| `TARGET_KUBECONFIG_SECRET_KEY` | no | `kubeconfig` | Key inside that Secret. | +| `SCANNER_IMAGE` | no | from Snapshot `tls-scanner` component | Override to pin a released scanner image. | +| `COMPONENT_NAME` | no | `tls-scanner` | Snapshot component name used when `SCANNER_IMAGE` is empty. | +| `SCAN_NAMESPACE` | no | `tls-scanner` | Namespace on the **target** cluster where the Job is created. | +| `NAMESPACE_FILTER` | no | `""` (all pods) | Comma-separated namespaces to scan. | +| `PQC_CHECK` | no | `"false"` | Pass `--pqc-check` when `"true"`. | +| `TLS_PROFILE_TYPE` | no | `""` | Pass `--tls-profile-type` (Old/Intermediate/Modern). | +| `LIMIT_IPS` | no | `"0"` | Cap IPs scanned (`0` = unlimited). | +| `SCANNER_PARALLEL` | no | `"4"` | Job `-j` concurrency. | +| `JOB_TIMEOUT_SECONDS` | no | `"3600"` | Wait budget for the Job. | + +The pipeline grants `cluster-reader`, `privileged` SCC, and a small +`tls-scanner-cross-namespace` ClusterRole (pods/exec + ingress/kubelet config +reads) to a ServiceAccount in `SCAN_NAMESPACE`, deploys the Job, waits for +`Scanner finished with exit code:`, copies `results.json` / `results.csv` / +`results.xml` / `scan.log`, then cleans up. It writes Tekton result +`TEST_OUTPUT` as: + +```json +{"result":"SUCCESS|FAILURE|ERROR|WARNING","timestamp":"...","note":"...","failures":0,"successes":1,"warnings":0} +``` + +That is the format the Integration Service uses for GitHub PR checks. + +### Target cluster privileges + +The kubeconfig in `TARGET_KUBECONFIG_SECRET` must be able to: + +- create namespaces / ServiceAccounts / Jobs in `SCAN_NAMESPACE` +- create ClusterRole / ClusterRoleBinding (or have them pre-created) +- `oc adm policy add-cluster-role-to-user` / `add-scc-to-user` +- `oc cp` from the scanner pod + +If your ephemeral env provisioning already creates a restricted SA, pre-create +the RBAC and tighten the pipeline later. + +## Standardized test result + +Integration Service recognizes the Tekton result named **`TEST_OUTPUT`** only. +Custom result names (e.g. a plain `PASSED` string) will not appear as PR checks. + +## Relationship to openshift/release CI + +Prow jobs in `openshift/release` continue to run unit tests, image builds, and +periodic full-cluster scans for core OpenShift. This Konflux packaging is for +**layered-product Konflux workflows** and for building/testing the scanner image +itself under Konflux — it does not replace the existing Prow periodics. + +## Phase notes + +- **Phase 1 (this directory):** onboard build + smoke/self-test + reusable + compliance pipeline for consumers that already have a kubeconfig Secret. +- **Later:** optional second ITS for scanning core OCP components under Konflux; + tighten RBAC; pin consumer `revision` values to released tags. diff --git a/konflux/application.yaml b/konflux/application.yaml new file mode 100644 index 00000000..36256054 --- /dev/null +++ b/konflux/application.yaml @@ -0,0 +1,9 @@ +# Apply to the Konflux cluster: oc apply -f application.yaml +# Not watched by Konflux from git -- see konflux/README.md. +apiVersion: appstudio.redhat.com/v1alpha1 +kind: Application +metadata: + name: tls-scanner + namespace: ocp-isc-tenant +spec: + displayName: TLS Scanner diff --git a/konflux/component.yaml b/konflux/component.yaml new file mode 100644 index 00000000..d6fb6aeb --- /dev/null +++ b/konflux/component.yaml @@ -0,0 +1,42 @@ +# Apply to the Konflux cluster: oc apply -f component.yaml +# Not watched by Konflux from git -- see konflux/README.md. +# +# The `configure-pac` annotation makes Konflux open a separate PR against this repo with +# generated .tekton/tls-scanner-pull-request.yaml and .tekton/tls-scanner-push.yaml build +# PipelineRuns. Do not hand-write those files here. +# +# dockerfileUrl points at Dockerfile.local (public golang + UBI9 bases) so Konflux can +# build without registry.ci.openshift.org credentials. The CI Dockerfile remains for +# openshift/release image builds. +apiVersion: appstudio.redhat.com/v1alpha1 +kind: Component +metadata: + name: tls-scanner + namespace: ocp-isc-tenant + annotations: + build.appstudio.openshift.io/request: configure-pac + git-provider: github +spec: + application: tls-scanner + componentName: tls-scanner + source: + git: + url: https://github.com/openshift/tls-scanner.git + revision: main + dockerfileUrl: Dockerfile.local +--- +# Provisions a Quay repo and sets Component.spec.containerImage automatically. +apiVersion: appstudio.redhat.com/v1alpha1 +kind: ImageRepository +metadata: + name: tls-scanner + namespace: ocp-isc-tenant + annotations: + image-controller.appstudio.redhat.com/update-component-image: "true" + labels: + appstudio.redhat.com/application: tls-scanner + appstudio.redhat.com/component: tls-scanner +spec: + image: + name: ocp-isc-tenant/tls-scanner + visibility: public diff --git a/konflux/integration-test-scenario.yaml b/konflux/integration-test-scenario.yaml new file mode 100644 index 00000000..f6dc6b51 --- /dev/null +++ b/konflux/integration-test-scenario.yaml @@ -0,0 +1,60 @@ +# Apply to the Konflux cluster: oc apply -f integration-test-scenario.yaml +# Not watched by Konflux from git -- see konflux/README.md. +# +# To validate this on the feature/PR branch before merging, temporarily change +# spec.resolverRef.params[revision] below to the branch name, re-apply, then +# open/update the PR. Switch it back to `main` before merging. +apiVersion: appstudio.redhat.com/v1beta2 +kind: IntegrationTestScenario +metadata: + name: tls-scanner-self-test + namespace: ocp-isc-tenant + labels: + # Failures won't block the Snapshot/PR from passing while this test is being bootstrapped. + test.appstudio.openshift.io/optional: "true" +spec: + application: tls-scanner + contexts: + - description: Smoke + TLS fixture Job scan against the just-built tls-scanner image. + name: application + resolverRef: + resolver: git + params: + - name: url + value: https://github.com/openshift/tls-scanner.git + # TEMP: point at the feature branch for PR validation. Change back to main before merge. + - name: revision + value: feat/konflux-tls-scan + - name: pathInRepo + value: konflux/pipelines/tls-scanner-self-test.yaml + params: + - name: COMPONENT_NAME + value: tls-scanner +--- +# Optional lighter check: image --version/--help only. Keep or remove once +# tls-scanner-self-test is stable in your tenant. +apiVersion: appstudio.redhat.com/v1beta2 +kind: IntegrationTestScenario +metadata: + name: tls-scanner-smoke-test + namespace: ocp-isc-tenant + labels: + test.appstudio.openshift.io/optional: "true" +spec: + application: tls-scanner + contexts: + - description: Confirm the tls-scanner image is runnable. + name: application + resolverRef: + resolver: git + params: + - name: url + value: https://github.com/openshift/tls-scanner.git + # TEMP: point at the feature branch for PR validation. Change back to main before merge. + - name: revision + value: feat/konflux-tls-scan + - name: pathInRepo + value: konflux/pipelines/tls-scanner-smoke-test.yaml + params: + - name: COMPONENT_NAME + value: tls-scanner diff --git a/konflux/pipelines/tls-scanner-compliance-scan.yaml b/konflux/pipelines/tls-scanner-compliance-scan.yaml new file mode 100644 index 00000000..10557773 --- /dev/null +++ b/konflux/pipelines/tls-scanner-compliance-scan.yaml @@ -0,0 +1,365 @@ +# Fetched at run time by Konflux via a layered product's IntegrationTestScenario +# git resolver (not applied to the cluster directly). See konflux/README.md +# "Using tls-scanner from a layered product". +# +# Deploys the tls-scanner Job into a target cluster (kubeconfig from +# TARGET_KUBECONFIG_SECRET), waits for completion, collects artifacts, and emits +# the standardized TEST_OUTPUT result the Integration Service uses for GitHub +# PR checks. +# +# Pin resolverRef.revision to a release tag or known-good commit — do not track +# main unless you intentionally want breaking pipeline changes. +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: tls-scanner-compliance-scan + labels: + app.kubernetes.io/name: tls-scanner-compliance-scan + app.kubernetes.io/part-of: tls-scanner +spec: + description: | + Deploy tls-scanner as an in-cluster Job against a target environment and + report TLS compliance via TEST_OUTPUT. Layered products supply a kubeconfig + Secret for their ephemeral test cluster and optional scan filters. + params: + - name: SNAPSHOT + description: Snapshot of the application under test, injected by Konflux. + type: string + default: '{"components":[]}' + - name: COMPONENT_NAME + description: Name of the tls-scanner component in SNAPSHOT when SCANNER_IMAGE is empty. + type: string + default: tls-scanner + - name: SCANNER_IMAGE + description: > + Full tls-scanner image reference. When empty, resolved from SNAPSHOT + using COMPONENT_NAME. Layered products should usually pin a released tag. + type: string + default: "" + - name: TARGET_KUBECONFIG_SECRET + description: Name of a Secret in the PipelineRun namespace holding the target kubeconfig. + type: string + - name: TARGET_KUBECONFIG_SECRET_KEY + description: Key within TARGET_KUBECONFIG_SECRET that contains the kubeconfig. + type: string + default: kubeconfig + - name: SCAN_NAMESPACE + description: Namespace in the target cluster where the scanner Job is deployed. + type: string + default: tls-scanner + - name: NAMESPACE_FILTER + description: Comma-separated namespaces to scan. Empty means scan all pods. + type: string + default: "" + - name: PQC_CHECK + description: When "true", pass --pqc-check to the scanner Job. + type: string + default: "false" + - name: TLS_PROFILE_TYPE + description: Optional expected TLS profile (Old, Intermediate, Modern). + type: string + default: "" + - name: LIMIT_IPS + description: Cap number of IPs to scan (0 = no limit). + type: string + default: "0" + - name: SCANNER_PARALLEL + description: Concurrent scan workers for the Job (-j). + type: string + default: "4" + - name: JOB_TIMEOUT_SECONDS + description: Max seconds to wait for the scanner Job to finish. + type: string + default: "3600" + results: + - name: TEST_OUTPUT + description: Standardized Konflux test result JSON. + value: $(tasks.run-scan.results.TEST_OUTPUT) + tasks: + - name: resolve-image + params: + - name: SNAPSHOT + value: $(params.SNAPSHOT) + - name: COMPONENT_NAME + value: $(params.COMPONENT_NAME) + - name: SCANNER_IMAGE + value: $(params.SCANNER_IMAGE) + taskSpec: + params: + - name: SNAPSHOT + - name: COMPONENT_NAME + - name: SCANNER_IMAGE + results: + - name: SCANNER_IMAGE + description: Resolved tls-scanner container image. + steps: + - name: resolve + image: quay.io/konflux-ci/konflux-test:stable + script: | + #!/bin/bash + set -euo pipefail + override="$(params.SCANNER_IMAGE)" + if [[ -n "${override}" ]]; then + echo -n "${override}" | tee "$(results.SCANNER_IMAGE.path)" + exit 0 + fi + image=$(jq -r --arg name "$(params.COMPONENT_NAME)" \ + '.components[]? | select(.name == $name) | .containerImage' \ + <<< "$(params.SNAPSHOT)" | head -n1) + if [[ -z "${image}" || "${image}" == "null" ]]; then + echo "SCANNER_IMAGE empty and no containerImage for component $(params.COMPONENT_NAME) in SNAPSHOT" >&2 + exit 1 + fi + echo -n "${image}" | tee "$(results.SCANNER_IMAGE.path)" + + - name: run-scan + runAfter: + - resolve-image + params: + - name: SCANNER_IMAGE + value: $(tasks.resolve-image.results.SCANNER_IMAGE) + - name: TARGET_KUBECONFIG_SECRET + value: $(params.TARGET_KUBECONFIG_SECRET) + - name: TARGET_KUBECONFIG_SECRET_KEY + value: $(params.TARGET_KUBECONFIG_SECRET_KEY) + - name: SCAN_NAMESPACE + value: $(params.SCAN_NAMESPACE) + - name: NAMESPACE_FILTER + value: $(params.NAMESPACE_FILTER) + - name: PQC_CHECK + value: $(params.PQC_CHECK) + - name: TLS_PROFILE_TYPE + value: $(params.TLS_PROFILE_TYPE) + - name: LIMIT_IPS + value: $(params.LIMIT_IPS) + - name: SCANNER_PARALLEL + value: $(params.SCANNER_PARALLEL) + - name: JOB_TIMEOUT_SECONDS + value: $(params.JOB_TIMEOUT_SECONDS) + taskSpec: + params: + - name: SCANNER_IMAGE + - name: TARGET_KUBECONFIG_SECRET + - name: TARGET_KUBECONFIG_SECRET_KEY + - name: SCAN_NAMESPACE + - name: NAMESPACE_FILTER + - name: PQC_CHECK + - name: TLS_PROFILE_TYPE + - name: LIMIT_IPS + - name: SCANNER_PARALLEL + - name: JOB_TIMEOUT_SECONDS + results: + - name: TEST_OUTPUT + description: Standardized Konflux test result. + steps: + - name: deploy-wait-collect + image: quay.io/konflux-ci/konflux-test:stable + env: + - name: SCANNER_IMAGE + value: $(params.SCANNER_IMAGE) + - name: TARGET_KUBECONFIG_SECRET + value: $(params.TARGET_KUBECONFIG_SECRET) + - name: TARGET_KUBECONFIG_SECRET_KEY + value: $(params.TARGET_KUBECONFIG_SECRET_KEY) + - name: SCAN_NAMESPACE + value: $(params.SCAN_NAMESPACE) + - name: NAMESPACE_FILTER + value: $(params.NAMESPACE_FILTER) + - name: PQC_CHECK + value: $(params.PQC_CHECK) + - name: TLS_PROFILE_TYPE + value: $(params.TLS_PROFILE_TYPE) + - name: LIMIT_IPS + value: $(params.LIMIT_IPS) + - name: SCANNER_PARALLEL + value: $(params.SCANNER_PARALLEL) + - name: JOB_TIMEOUT_SECONDS + value: $(params.JOB_TIMEOUT_SECONDS) + script: | + #!/bin/bash + set -uo pipefail + + write_test_output() { + local result="$1" + local note="$2" + local TEST_OUTPUT + TEST_OUTPUT=$(jq -rc -n \ + --arg date "$(date -u --iso-8601=seconds)" \ + --arg result "${result}" \ + --arg note "${note}" \ + '{result: $result, timestamp: $date, note: $note, failures: (if $result == "SUCCESS" then 0 else 1 end), successes: (if $result == "SUCCESS" then 1 else 0 end), warnings: 0}') + echo -n "${TEST_OUTPUT}" | tee "$(results.TEST_OUTPUT.path)" + } + + ARTIFACTS=/tmp/tls-scanner-artifacts + mkdir -p "${ARTIFACTS}" + + ns="${NAMESPACE:-$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)}" + if ! oc get secret "${TARGET_KUBECONFIG_SECRET}" -n "${ns}" \ + -o "jsonpath={.data.${TARGET_KUBECONFIG_SECRET_KEY}}" | base64 -d > /tmp/kubeconfig; then + write_test_output "ERROR" "failed to read kubeconfig secret ${TARGET_KUBECONFIG_SECRET}" + exit 1 + fi + export KUBECONFIG=/tmp/kubeconfig + chmod 600 "${KUBECONFIG}" + + JOB_NAME="tls-scanner-job" + SA_NAME="tls-scanner" + + cleanup() { + oc delete job "${JOB_NAME}" -n "${SCAN_NAMESPACE}" --ignore-not-found=true || true + oc adm policy remove-cluster-role-from-user cluster-reader -z "${SA_NAME}" -n "${SCAN_NAMESPACE}" 2>/dev/null || true + oc adm policy remove-cluster-role-from-user tls-scanner-cross-namespace -z "${SA_NAME}" -n "${SCAN_NAMESPACE}" 2>/dev/null || true + oc adm policy remove-scc-from-user privileged -z "${SA_NAME}" -n "${SCAN_NAMESPACE}" 2>/dev/null || true + } + trap cleanup EXIT + + oc get ns "${SCAN_NAMESPACE}" >/dev/null 2>&1 || oc create ns "${SCAN_NAMESPACE}" + oc get sa "${SA_NAME}" -n "${SCAN_NAMESPACE}" >/dev/null 2>&1 || oc create sa "${SA_NAME}" -n "${SCAN_NAMESPACE}" + oc adm policy add-cluster-role-to-user cluster-reader -z "${SA_NAME}" -n "${SCAN_NAMESPACE}" + oc adm policy add-scc-to-user privileged -z "${SA_NAME}" -n "${SCAN_NAMESPACE}" || true + + python3 - <<'PY' + import json + role = { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": {"name": "tls-scanner-cross-namespace"}, + "rules": [ + {"apiGroups": [""], "resources": ["pods/exec"], "verbs": ["create"]}, + {"apiGroups": ["operator.openshift.io"], "resources": ["ingresscontrollers"], "verbs": ["get", "list"]}, + {"apiGroups": ["machineconfiguration.openshift.io"], "resources": ["kubeletconfigs"], "verbs": ["get", "list"]}, + ], + } + open("/tmp/tls-scanner-clusterrole.json", "w").write(json.dumps(role)) + PY + oc apply -f /tmp/tls-scanner-clusterrole.json + oc adm policy add-cluster-role-to-user tls-scanner-cross-namespace -z "${SA_NAME}" -n "${SCAN_NAMESPACE}" + + EXTRA_ARGS="" + if [[ -n "${NAMESPACE_FILTER}" ]]; then + EXTRA_ARGS+=" --namespace-filter $(echo "${NAMESPACE_FILTER}" | tr -d ' ')" + fi + if [[ "${PQC_CHECK}" == "true" ]]; then + EXTRA_ARGS+=" --pqc-check" + fi + if [[ -n "${TLS_PROFILE_TYPE}" ]]; then + EXTRA_ARGS+=" --tls-profile-type ${TLS_PROFILE_TYPE}" + fi + if [[ "${LIMIT_IPS}" != "0" && -n "${LIMIT_IPS}" ]]; then + EXTRA_ARGS+=" --limit-ips ${LIMIT_IPS}" + fi + + oc delete job "${JOB_NAME}" -n "${SCAN_NAMESPACE}" --ignore-not-found=true + + SCANNER_IMAGE="${SCANNER_IMAGE}" SCAN_NAMESPACE="${SCAN_NAMESPACE}" \ + JOB_NAME="${JOB_NAME}" SA_NAME="${SA_NAME}" SCANNER_PARALLEL="${SCANNER_PARALLEL}" \ + EXTRA_ARGS="${EXTRA_ARGS}" python3 - <<'PY' + import json, os + extra = os.environ.get("EXTRA_ARGS", "").strip() + cmd = ( + "set -uo pipefail; " + f"/usr/local/bin/tls-scanner --all-pods -j {os.environ['SCANNER_PARALLEL']} " + "--artifact-dir /artifacts --json-file /artifacts/results.json " + "--csv-file /artifacts/results.csv --junit-file /artifacts/results.xml " + f"--log-file /artifacts/scan.log {extra}; " + "exit_code=$?; " + 'echo "Scanner finished with exit code: $exit_code"; ' + "echo -n $exit_code > /artifacts/exit_code; " + "sleep 120; exit 0" + ) + job = { + "apiVersion": "batch/v1", + "kind": "Job", + "metadata": { + "name": os.environ["JOB_NAME"], + "namespace": os.environ["SCAN_NAMESPACE"], + }, + "spec": { + "backoffLimit": 1, + "template": { + "spec": { + "serviceAccountName": os.environ["SA_NAME"], + "restartPolicy": "Never", + "containers": [{ + "name": "tls-scanner", + "image": os.environ["SCANNER_IMAGE"], + "imagePullPolicy": "Always", + "securityContext": {"privileged": True}, + "command": ["/bin/sh", "-c", cmd], + "volumeMounts": [{"name": "artifacts", "mountPath": "/artifacts"}], + }], + "volumes": [{"name": "artifacts", "emptyDir": {}}], + } + }, + }, + } + open("/tmp/tls-scanner-job.json", "w").write(json.dumps(job)) + PY + oc apply -f /tmp/tls-scanner-job.json + + echo "Waiting for scanner pod..." + POD="" + for i in $(seq 1 60); do + POD=$(oc get pods -n "${SCAN_NAMESPACE}" -l job-name="${JOB_NAME}" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) + if [[ -n "${POD}" ]]; then + phase=$(oc get pod "${POD}" -n "${SCAN_NAMESPACE}" -o jsonpath='{.status.phase}' 2>/dev/null || echo Unknown) + if [[ "${phase}" == "Running" || "${phase}" == "Succeeded" ]]; then + break + fi + if [[ "${phase}" == "Failed" ]]; then + oc describe pod "${POD}" -n "${SCAN_NAMESPACE}" || true + write_test_output "ERROR" "scanner pod failed to start" + exit 1 + fi + fi + sleep 5 + done + if [[ -z "${POD}" ]]; then + write_test_output "ERROR" "scanner pod was never created" + exit 1 + fi + + echo "Waiting for scanner to finish (timeout ${JOB_TIMEOUT_SECONDS}s)..." + deadline=$(( $(date +%s) + JOB_TIMEOUT_SECONDS )) + while true; do + if oc logs "pod/${POD}" -n "${SCAN_NAMESPACE}" 2>/dev/null | grep -q "Scanner finished with exit code:"; then + break + fi + phase=$(oc get pod "${POD}" -n "${SCAN_NAMESPACE}" -o jsonpath='{.status.phase}' 2>/dev/null || echo Unknown) + if [[ "${phase}" == "Failed" ]]; then + write_test_output "ERROR" "scanner pod failed during run" + exit 1 + fi + if [[ $(date +%s) -ge ${deadline} ]]; then + write_test_output "ERROR" "timed out waiting for scanner Job" + exit 1 + fi + sleep 10 + done + + sleep 3 + oc cp "${SCAN_NAMESPACE}/${POD}:/artifacts/results.json" "${ARTIFACTS}/results.json" || true + oc cp "${SCAN_NAMESPACE}/${POD}:/artifacts/results.csv" "${ARTIFACTS}/results.csv" || true + oc cp "${SCAN_NAMESPACE}/${POD}:/artifacts/results.xml" "${ARTIFACTS}/results.xml" || true + oc cp "${SCAN_NAMESPACE}/${POD}:/artifacts/scan.log" "${ARTIFACTS}/scan.log" || true + oc cp "${SCAN_NAMESPACE}/${POD}:/artifacts/exit_code" "${ARTIFACTS}/exit_code" || echo "1" > "${ARTIFACTS}/exit_code" + + echo "=== scan.log (tail) ===" + tail -n 100 "${ARTIFACTS}/scan.log" 2>/dev/null || true + if [[ -f "${ARTIFACTS}/results.json" ]]; then + echo "=== results.json summary ===" + jq '{scanned_ips, total_ports: ([.ip_results[]?.port_results[]?] | length)}' "${ARTIFACTS}/results.json" || true + fi + + exit_code=$(cat "${ARTIFACTS}/exit_code" 2>/dev/null || echo 1) + if [[ "${exit_code}" == "0" ]]; then + note="tls-scanner completed successfully" + if [[ "${PQC_CHECK}" == "true" ]]; then + note="tls-scanner --pqc-check passed" + fi + write_test_output "SUCCESS" "${note}" + exit 0 + fi + write_test_output "FAILURE" "tls-scanner exited with code ${exit_code}" + exit 1 diff --git a/konflux/pipelines/tls-scanner-self-test.yaml b/konflux/pipelines/tls-scanner-self-test.yaml new file mode 100644 index 00000000..31a7557e --- /dev/null +++ b/konflux/pipelines/tls-scanner-self-test.yaml @@ -0,0 +1,291 @@ +# Fetched at run time by this repo's IntegrationTestScenario git resolver. +# Not applied to the cluster directly. See konflux/README.md. +# +# End-to-end self-test for a tls-scanner Snapshot build: +# 1) resolve the just-built image from SNAPSHOT +# 2) smoke-check --version / --help +# 3) deploy a short-lived TLS fixture pod in the PipelineRun namespace +# 4) deploy the scanner Job against that namespace using in-cluster credentials +# 5) emit standardized TEST_OUTPUT +# +# Requires the PipelineRun SA to create pods/jobs in its namespace and (for the +# scanner Job) cluster-reader + pods/exec + privileged SCC on the target +# cluster. Marked optional on the ITS while bootstrapping. +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: tls-scanner-self-test + labels: + app.kubernetes.io/name: tls-scanner-self-test + app.kubernetes.io/part-of: tls-scanner +spec: + description: Self-test a tls-scanner Snapshot with a local TLS fixture and Job-based scan. + params: + - name: SNAPSHOT + description: Snapshot of the application under test, injected by Konflux. + type: string + - name: COMPONENT_NAME + description: Name of the tls-scanner component in SNAPSHOT. + type: string + default: tls-scanner + results: + - name: TEST_OUTPUT + description: Standardized Konflux test result JSON. + value: $(tasks.self-test.results.TEST_OUTPUT) + tasks: + - name: self-test + params: + - name: SNAPSHOT + value: $(params.SNAPSHOT) + - name: COMPONENT_NAME + value: $(params.COMPONENT_NAME) + taskSpec: + params: + - name: SNAPSHOT + - name: COMPONENT_NAME + results: + - name: TEST_OUTPUT + description: Standardized Konflux test result. + steps: + - name: run-self-test + image: quay.io/konflux-ci/konflux-test:stable + env: + - name: SNAPSHOT + value: $(params.SNAPSHOT) + - name: COMPONENT_NAME + value: $(params.COMPONENT_NAME) + script: | + #!/bin/bash + set -uo pipefail + + write_test_output() { + local result="$1" + local note="$2" + local TEST_OUTPUT + TEST_OUTPUT=$(jq -rc -n \ + --arg date "$(date -u --iso-8601=seconds)" \ + --arg result "${result}" \ + --arg note "${note}" \ + '{result: $result, timestamp: $date, note: $note, failures: (if $result == "SUCCESS" then 0 else 1 end), successes: (if $result == "SUCCESS" then 1 else 0 end), warnings: 0}') + echo -n "${TEST_OUTPUT}" | tee "$(results.TEST_OUTPUT.path)" + } + + SCANNER_IMAGE=$(jq -r --arg name "${COMPONENT_NAME}" \ + '.components[]? | select(.name == $name) | .containerImage' \ + <<< "${SNAPSHOT}" | head -n1) + if [[ -z "${SCANNER_IMAGE}" || "${SCANNER_IMAGE}" == "null" ]]; then + write_test_output "ERROR" "no containerImage for component ${COMPONENT_NAME} in SNAPSHOT" + exit 1 + fi + echo "SCANNER_IMAGE=${SCANNER_IMAGE}" + + # --- Smoke: image is runnable --- + if ! oc run tls-scanner-smoke --rm -i --restart=Never --image="${SCANNER_IMAGE}" \ + --command -- /usr/local/bin/tls-scanner --version; then + write_test_output "FAILURE" "tls-scanner --version failed in smoke pod" + exit 1 + fi + + # --- Fixture + Job scan using in-cluster kubeconfig --- + ns=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace) + export ns + FIXTURE_NAME="tls-scanner-fixture" + JOB_NAME="tls-scanner-selftest-job" + SA_NAME="tls-scanner-selftest" + ARTIFACTS=/tmp/tls-scanner-artifacts + mkdir -p "${ARTIFACTS}" + + # Build a kubeconfig from the PipelineRun SA so the compliance-style + # Job deploy path is exercised against this same cluster. + TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt + APISERVER="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}" + kubectl config --kubeconfig=/tmp/kubeconfig set-cluster in-cluster \ + --server="${APISERVER}" --certificate-authority="${CA}" --embed-certs=true + kubectl config --kubeconfig=/tmp/kubeconfig set-credentials sa --token="${TOKEN}" + kubectl config --kubeconfig=/tmp/kubeconfig set-context in-cluster \ + --cluster=in-cluster --user=sa --namespace="${ns}" + kubectl config --kubeconfig=/tmp/kubeconfig use-context in-cluster + export KUBECONFIG=/tmp/kubeconfig + + cleanup() { + oc delete pod "${FIXTURE_NAME}" -n "${ns}" --ignore-not-found=true || true + oc delete job "${JOB_NAME}" -n "${ns}" --ignore-not-found=true || true + oc delete sa "${SA_NAME}" -n "${ns}" --ignore-not-found=true || true + oc adm policy remove-cluster-role-from-user cluster-reader -z "${SA_NAME}" -n "${ns}" 2>/dev/null || true + oc adm policy remove-cluster-role-from-user tls-scanner-cross-namespace -z "${SA_NAME}" -n "${ns}" 2>/dev/null || true + oc adm policy remove-scc-from-user privileged -z "${SA_NAME}" -n "${ns}" 2>/dev/null || true + } + trap cleanup EXIT + + # Deploy a TLS fixture (openssl s_server) listening on 8443. + python3 - <<'PY' + import json, os + ns = os.environ["ns"] + pod = { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "tls-scanner-fixture", + "namespace": ns, + "labels": {"app": "tls-scanner-fixture"}, + }, + "spec": { + "restartPolicy": "Never", + "containers": [{ + "name": "tls", + "image": "registry.access.redhat.com/ubi9/ubi-minimal:latest", + "command": ["/bin/bash", "-c"], + "args": [ + "microdnf install -y openssl >/dev/null 2>&1 || yum install -y openssl >/dev/null 2>&1 || true; " + "openssl req -x509 -newkey rsa:2048 -keyout /tmp/key.pem -out /tmp/cert.pem -days 1 -nodes -subj /CN=tls-scanner-fixture; " + "openssl s_server -accept 8443 -cert /tmp/cert.pem -key /tmp/key.pem -www" + ], + "ports": [{"containerPort": 8443, "protocol": "TCP", "name": "https"}], + "readinessProbe": { + "tcpSocket": {"port": 8443}, + "initialDelaySeconds": 5, + "periodSeconds": 2, + }, + }], + }, + } + open("/tmp/fixture.json", "w").write(json.dumps(pod)) + PY + oc apply -f /tmp/fixture.json + if ! oc wait --for=condition=Ready pod/"${FIXTURE_NAME}" -n "${ns}" --timeout=180s; then + oc describe pod "${FIXTURE_NAME}" -n "${ns}" || true + write_test_output "ERROR" "TLS fixture pod did not become Ready" + exit 1 + fi + + oc get sa "${SA_NAME}" -n "${ns}" >/dev/null 2>&1 || oc create sa "${SA_NAME}" -n "${ns}" + oc adm policy add-cluster-role-to-user cluster-reader -z "${SA_NAME}" -n "${ns}" || true + oc adm policy add-scc-to-user privileged -z "${SA_NAME}" -n "${ns}" || true + + python3 - <<'PY' + import json + role = { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": {"name": "tls-scanner-cross-namespace"}, + "rules": [ + {"apiGroups": [""], "resources": ["pods/exec"], "verbs": ["create"]}, + {"apiGroups": ["operator.openshift.io"], "resources": ["ingresscontrollers"], "verbs": ["get", "list"]}, + {"apiGroups": ["machineconfiguration.openshift.io"], "resources": ["kubeletconfigs"], "verbs": ["get", "list"]}, + ], + } + open("/tmp/tls-scanner-clusterrole.json", "w").write(json.dumps(role)) + PY + oc apply -f /tmp/tls-scanner-clusterrole.json || true + oc adm policy add-cluster-role-to-user tls-scanner-cross-namespace -z "${SA_NAME}" -n "${ns}" || true + + oc delete job "${JOB_NAME}" -n "${ns}" --ignore-not-found=true + + SCANNER_IMAGE="${SCANNER_IMAGE}" ns="${ns}" JOB_NAME="${JOB_NAME}" SA_NAME="${SA_NAME}" python3 - <<'PY' + import json, os + ns = os.environ["ns"] + cmd = ( + "set -uo pipefail; " + f"/usr/local/bin/tls-scanner --all-pods -j 2 --namespace-filter {ns} " + "--artifact-dir /artifacts --json-file /artifacts/results.json " + "--csv-file /artifacts/results.csv --junit-file /artifacts/results.xml " + "--log-file /artifacts/scan.log --limit-ips 5; " + "exit_code=$?; " + 'echo "Scanner finished with exit code: $exit_code"; ' + "echo -n $exit_code > /artifacts/exit_code; " + "sleep 90; exit 0" + ) + job = { + "apiVersion": "batch/v1", + "kind": "Job", + "metadata": {"name": os.environ["JOB_NAME"], "namespace": ns}, + "spec": { + "backoffLimit": 1, + "template": { + "spec": { + "serviceAccountName": os.environ["SA_NAME"], + "restartPolicy": "Never", + "containers": [{ + "name": "tls-scanner", + "image": os.environ["SCANNER_IMAGE"], + "imagePullPolicy": "Always", + "securityContext": {"privileged": True}, + "command": ["/bin/sh", "-c", cmd], + "volumeMounts": [{"name": "artifacts", "mountPath": "/artifacts"}], + }], + "volumes": [{"name": "artifacts", "emptyDir": {}}], + } + }, + }, + } + open("/tmp/tls-scanner-job.json", "w").write(json.dumps(job)) + PY + if ! oc apply -f /tmp/tls-scanner-job.json; then + write_test_output "ERROR" "failed to create scanner Job (RBAC?)" + exit 1 + fi + + POD="" + for i in $(seq 1 60); do + POD=$(oc get pods -n "${ns}" -l job-name="${JOB_NAME}" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) + if [[ -n "${POD}" ]]; then + phase=$(oc get pod "${POD}" -n "${ns}" -o jsonpath='{.status.phase}' 2>/dev/null || echo Unknown) + if [[ "${phase}" == "Running" || "${phase}" == "Succeeded" ]]; then + break + fi + if [[ "${phase}" == "Failed" ]]; then + oc describe pod "${POD}" -n "${ns}" || true + write_test_output "ERROR" "self-test scanner pod failed to start" + exit 1 + fi + fi + sleep 5 + done + if [[ -z "${POD}" ]]; then + write_test_output "ERROR" "self-test scanner pod was never created" + exit 1 + fi + + deadline=$(( $(date +%s) + 900 )) + while true; do + if oc logs "pod/${POD}" -n "${ns}" 2>/dev/null | grep -q "Scanner finished with exit code:"; then + break + fi + phase=$(oc get pod "${POD}" -n "${ns}" -o jsonpath='{.status.phase}' 2>/dev/null || echo Unknown) + if [[ "${phase}" == "Failed" ]]; then + write_test_output "ERROR" "self-test scanner pod failed during run" + exit 1 + fi + if [[ $(date +%s) -ge ${deadline} ]]; then + write_test_output "ERROR" "timed out waiting for self-test scanner Job" + exit 1 + fi + sleep 10 + done + + sleep 3 + oc cp "${ns}/${POD}:/artifacts/results.json" "${ARTIFACTS}/results.json" || true + oc cp "${ns}/${POD}:/artifacts/scan.log" "${ARTIFACTS}/scan.log" || true + oc cp "${ns}/${POD}:/artifacts/exit_code" "${ARTIFACTS}/exit_code" || echo "1" > "${ARTIFACTS}/exit_code" + + echo "=== scan.log (tail) ===" + tail -n 80 "${ARTIFACTS}/scan.log" 2>/dev/null || true + if [[ -f "${ARTIFACTS}/results.json" ]]; then + jq '{scanned_ips, total_ports: ([.ip_results[]?.port_results[]?] | length)}' "${ARTIFACTS}/results.json" || true + fi + + exit_code=$(cat "${ARTIFACTS}/exit_code" 2>/dev/null || echo 1) + if [[ ! -f "${ARTIFACTS}/results.json" ]]; then + write_test_output "FAILURE" "scanner produced no results.json (exit ${exit_code})" + exit 1 + fi + + ports=$(jq '[.ip_results[]?.port_results[]?] | length' "${ARTIFACTS}/results.json" 2>/dev/null || echo 0) + if [[ "${ports}" == "0" ]]; then + write_test_output "FAILURE" "scanner found zero ports in self-test namespace" + exit 1 + fi + + write_test_output "SUCCESS" "self-test scanned ${ports} port(s); smoke + fixture Job path OK" + exit 0 diff --git a/konflux/pipelines/tls-scanner-smoke-test.yaml b/konflux/pipelines/tls-scanner-smoke-test.yaml new file mode 100644 index 00000000..0a74dc88 --- /dev/null +++ b/konflux/pipelines/tls-scanner-smoke-test.yaml @@ -0,0 +1,87 @@ +# Fetched at run time by Konflux via the git resolver configured in +# konflux/integration-test-scenario.yaml (not applied to the cluster directly). +# +# Phase 1 smoke test: confirms the just-built tls-scanner image is runnable by executing +# `tls-scanner --version` and `--help` inside it, and reports the standardized TEST_OUTPUT +# result the Integration Service uses to post the PR check. +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: tls-scanner-smoke-test +spec: + params: + - name: SNAPSHOT + description: Snapshot of the application under test, injected by Konflux. + type: string + - name: COMPONENT_NAME + description: Name of the component under test, injected by Konflux via label. + type: string + default: tls-scanner + tasks: + - name: parse-metadata + params: + - name: SNAPSHOT + value: $(params.SNAPSHOT) + - name: COMPONENT_NAME + value: $(params.COMPONENT_NAME) + taskSpec: + params: + - name: SNAPSHOT + - name: COMPONENT_NAME + results: + - name: COMPONENT_IMAGE + description: containerImage of COMPONENT_NAME extracted from the Snapshot. + steps: + - name: extract-image + image: quay.io/konflux-ci/konflux-test:stable + script: | + #!/bin/bash + set -euo pipefail + image=$(jq -r --arg name "$(params.COMPONENT_NAME)" \ + '.components[] | select(.name == $name) | .containerImage' \ + <<< "$(params.SNAPSHOT)") + if [[ -z "${image}" || "${image}" == "null" ]]; then + echo "no containerImage found for component $(params.COMPONENT_NAME) in Snapshot" >&2 + exit 1 + fi + echo -n "${image}" | tee "$(results.COMPONENT_IMAGE.path)" + - name: smoke-test + runAfter: + - parse-metadata + params: + - name: COMPONENT_IMAGE + value: $(tasks.parse-metadata.results.COMPONENT_IMAGE) + taskSpec: + params: + - name: COMPONENT_IMAGE + results: + - name: TEST_OUTPUT + description: Standardized Konflux test result. + steps: + - name: run-scanner-smoke-test + image: $(params.COMPONENT_IMAGE) + script: | + #!/bin/bash + set -uo pipefail + + result="SUCCESS" + note="" + + if ! /usr/local/bin/tls-scanner --version; then + result="FAILURE" + note="tls-scanner --version failed" + fi + + if ! /usr/local/bin/tls-scanner --help >/dev/null; then + result="FAILURE" + note="${note:+$note; }tls-scanner --help failed" + fi + + TEST_OUTPUT=$(jq -rc -n \ + --arg date "$(date -u --iso-8601=seconds)" \ + --arg result "${result}" \ + --arg note "${note}" \ + '{result: $result, timestamp: $date, note: $note, failures: (if $result == "SUCCESS" then 0 else 1 end), successes: (if $result == "SUCCESS" then 1 else 0 end), warnings: 0}') + echo -n "${TEST_OUTPUT}" | tee "$(results.TEST_OUTPUT.path)" + + [[ "${result}" == "SUCCESS" ]]