diff --git a/.github/workflows/ci_cross_repo_integration.yml b/.github/workflows/ci_cross_repo_integration.yml index 185d2cd3..8ff7e842 100644 --- a/.github/workflows/ci_cross_repo_integration.yml +++ b/.github/workflows/ci_cross_repo_integration.yml @@ -1,8 +1,8 @@ # Cross-repo integration test workflow. # Builds complyctl from the PR branch, checks out complytime-providers@main, -# builds the Ampel provider binary, installs snappy and ampel, and runs the -# cross-repo integration test script to validate the full complyctl + Ampel -# provider pipeline end-to-end. +# builds provider binaries (ampel + opa), installs snappy, ampel, and conftest, +# and runs the cross-repo integration test script to validate the full +# complyctl + provider pipeline end-to-end. name: Cross-Repo Integration Test on: @@ -58,6 +58,9 @@ jobs: - name: Install ampel uses: carabiner-dev/actions/install/ampel@94f29392187fe5082d1195a7d4cae3a7ddf09d9c # v1.2.1 + - name: Install conftest + run: go install github.com/open-policy-agent/conftest@v0.68.2 + - name: Run cross-repo integration test env: PROVIDERS_BIN_DIR: ${{ github.workspace }}/_providers/bin diff --git a/.trivyignore.yaml b/.trivyignore.yaml new file mode 100644 index 00000000..4a8828b9 --- /dev/null +++ b/.trivyignore.yaml @@ -0,0 +1,85 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Trivy misconfig suppressions for K8s Deployment test fixtures. +# These files are intentionally non-compliant or minimally compliant +# to validate OPA provider policy evaluation. They are not deployed +# infrastructure. + +misconfigurations: + - id: KSV-0118 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + - "tests/cross-repo/testdata/test-deployment-good.yaml" + statement: Intentional test fixture for OPA provider validation + - id: KSV-0014 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + - "tests/cross-repo/testdata/test-deployment-good.yaml" + statement: Intentional test fixture for OPA provider validation + - id: KSV-0001 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + - "tests/cross-repo/testdata/test-deployment-good.yaml" + statement: Intentional test fixture for OPA provider validation + - id: KSV-0012 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + statement: Intentional test fixture for OPA provider validation + - id: KSV-0104 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + - "tests/cross-repo/testdata/test-deployment-good.yaml" + statement: Intentional test fixture for OPA provider validation + - id: KSV-0003 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + - "tests/cross-repo/testdata/test-deployment-good.yaml" + statement: Intentional test fixture for OPA provider validation + - id: KSV-0004 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + - "tests/cross-repo/testdata/test-deployment-good.yaml" + statement: Intentional test fixture for OPA provider validation + - id: KSV-0011 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + statement: Intentional test fixture for OPA provider validation + - id: KSV-0015 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + - "tests/cross-repo/testdata/test-deployment-good.yaml" + statement: Intentional test fixture for OPA provider validation + - id: KSV-0016 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + - "tests/cross-repo/testdata/test-deployment-good.yaml" + statement: Intentional test fixture for OPA provider validation + - id: KSV-0018 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + statement: Intentional test fixture for OPA provider validation + - id: KSV-0020 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + - "tests/cross-repo/testdata/test-deployment-good.yaml" + statement: Intentional test fixture for OPA provider validation + - id: KSV-0021 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + - "tests/cross-repo/testdata/test-deployment-good.yaml" + statement: Intentional test fixture for OPA provider validation + - id: KSV-0030 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + - "tests/cross-repo/testdata/test-deployment-good.yaml" + statement: Intentional test fixture for OPA provider validation + - id: KSV-0106 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + - "tests/cross-repo/testdata/test-deployment-good.yaml" + statement: Intentional test fixture for OPA provider validation + - id: KSV-0110 + paths: + - "tests/cross-repo/testdata/test-deployment-bad.yaml" + - "tests/cross-repo/testdata/test-deployment-good.yaml" + statement: Intentional test fixture for OPA provider validation diff --git a/AGENTS.md b/AGENTS.md index 99e246e1..f232ee18 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,7 +126,7 @@ scripts/ # maintenance scripts (SPDX checks, workflow setup) specs/ # Speckit strategic specifications (NNN-*/ format) tests/ ├── behavioral/ # behavioral test scenarios -├── cross-repo/ # cross-repo integration tests (complyctl + ampel provider) +├── cross-repo/ # cross-repo integration tests (complyctl + ampel + opa providers) ├── e2e/ # E2E tests (build-tag gated: -tags=e2e) └── integration_test.sh # shell-based integration test vendor/ # vendored dependencies diff --git a/docs/TESTING_ENVIRONMENT.md b/docs/TESTING_ENVIRONMENT.md index cda085c9..d2aa9cbd 100644 --- a/docs/TESTING_ENVIRONMENT.md +++ b/docs/TESTING_ENVIRONMENT.md @@ -233,11 +233,11 @@ complyctl get # Expected: "Synchronization completed." # Generate for the OPA provider -complyctl generate --policy-id test-opa-bp +complyctl generate --policy-id test-opa-k8s # Expected: "Generation completed." # Run a scan against the test deployment -complyctl scan --policy-id test-opa-bp +complyctl scan --policy-id test-opa-k8s # Expected: Scan results for container security requirements ``` diff --git a/tests/cross-repo/cross_repo_integration_test.sh b/tests/cross-repo/cross_repo_integration_test.sh index e65136d3..18b05503 100755 --- a/tests/cross-repo/cross_repo_integration_test.sh +++ b/tests/cross-repo/cross_repo_integration_test.sh @@ -1,12 +1,13 @@ #!/usr/bin/env bash # SPDX-License-Identifier: Apache-2.0 # -# Cross-repo integration test: complyctl + complytime-provider-ampel. -# Validates the full get → generate → scan pipeline using real binaries -# and the real GitHub API via snappy and ampel. +# Cross-repo integration test: complyctl + complytime-providers (ampel + opa). +# Validates the full get → generate → scan pipeline using real binaries. +# Ampel tests use the real GitHub API via snappy and ampel. +# OPA tests use a local K8s Deployment fixture evaluated by conftest. # # Required environment variables: -# PROVIDERS_BIN_DIR Directory containing complyctl-provider-ampel +# PROVIDERS_BIN_DIR Directory containing complyctl-provider-ampel and complyctl-provider-opa # GITHUB_TOKEN GitHub token with read access to public repositories # # Run locally: make test-cross-repo PROVIDERS_BIN_DIR=/path/to/providers/bin @@ -22,6 +23,7 @@ TESTDATA_DIR="${REPO_ROOT}/tests/cross-repo/testdata" REGISTRY_PORT="${GEMARA_SERVICE_PORT:-8765}" REGISTRY_URL="http://localhost:${REGISTRY_PORT}" POLICY_ID="test-ampel-bp" +OPA_POLICY_ID="test-opa-k8s" WORK_DIR="" TEST_HOME="" @@ -133,6 +135,18 @@ if [[ ! -x "${PROVIDER_BINARY}" ]]; then exit 1 fi +OPA_PROVIDER_BINARY="${PROVIDERS_BIN_DIR}/complyctl-provider-opa" +if [[ ! -x "${OPA_PROVIDER_BINARY}" ]]; then + echo "FATAL: complyctl-provider-opa not found or not executable at ${OPA_PROVIDER_BINARY}" + echo " Build complytime-providers first and set PROVIDERS_BIN_DIR to its bin/ directory." + exit 1 +fi + +if ! command -v conftest >/dev/null 2>&1; then + echo "FATAL: 'conftest' is required but not installed. The OPA provider requires conftest." + exit 1 +fi + if [[ ! -x "${BINARY}" ]]; then echo "FATAL: complyctl binary not found at ${BINARY}. Run 'make build' first." exit 1 @@ -161,9 +175,10 @@ TEST_HOME="$(mktemp -d)" WORK_DIR="$(mktemp -d)" export HOME="${TEST_HOME}" -# Install provider binary into the isolated home +# Install provider binaries into the isolated home mkdir -p "${TEST_HOME}/.complytime/providers" cp "${PROVIDER_BINARY}" "${TEST_HOME}/.complytime/providers/" +cp "${OPA_PROVIDER_BINARY}" "${TEST_HOME}/.complytime/providers/" echo " HOME=${TEST_HOME}" echo " WORK=${WORK_DIR}" @@ -176,7 +191,11 @@ sed "s|http://localhost:8765|${REGISTRY_URL}|" \ mkdir -p "${WORK_DIR}/.complytime/ampel/granular-policies" cp "${TESTDATA_DIR}/granular-policies/block-force-push.json" \ "${WORK_DIR}/.complytime/ampel/granular-policies/" -echo " Workspace config and granular policy copied." + +# Copy OPA test fixture into the workspace. +# Start with the non-compliant (bad) fixture; the compliant test swaps it. +cp "${TESTDATA_DIR}/test-deployment-bad.yaml" "${WORK_DIR}/test-deployment.yaml" +echo " Workspace config, granular policy, and OPA test fixture copied." # Start mock registry. # 30 retries (15s) — longer than integration_test.sh (15 retries / 7.5s) because @@ -293,12 +312,108 @@ test_generate_bad_policy() { assert_contains "generate bad policy: error message" "${out}" "not found" } +# --- test_get_opa --- + +test_get_opa() { + FOUND_FILE="" + echo "" + echo "=== test_get_opa ===" + # test_get already ran complyctl get which pulls all policies and complypacks. + # Verify OPA-specific artifacts were fetched. + assert_file_exists "get opa: oci-layout exists" \ + "${TEST_HOME}/.complytime/policies/policies/test-opa-policy/oci-layout" + + # Complypack cache uses evaluator-id/version/ structure. + # Find any content.tar.gz under the opa evaluator directory. + local complypack_match + complypack_match=$(find "${TEST_HOME}/.complytime/complypacks/opa/" \ + -name "content.tar.gz" -print -quit 2>/dev/null) || true + if [[ -n "${complypack_match}" && -s "${complypack_match}" ]]; then + pass "get opa: complypack cached" + else + fail "get opa: complypack cached: no content.tar.gz under complypacks/opa/" + fi +} + +# --- test_generate_opa --- + +test_generate_opa() { + FOUND_FILE="" + echo "" + echo "=== test_generate_opa ===" + local out rc=0 + out="$(run_complyctl generate --policy-id "${OPA_POLICY_ID}")" || rc=$? + if [[ "${rc}" -ne 0 ]]; then + fail "generate opa: unexpected exit code ${rc}" + echo "${out}" | sanitize_output >&2 + return + fi + echo "${out}" | sanitize_output + assert_contains "generate opa: completed" "${out}" "Generation completed." +} + +# --- test_scan_opa (non-compliant fixture — expects failures) --- + +test_scan_opa() { + FOUND_FILE="" + echo "" + echo "=== test_scan_opa ===" + local out rc=0 + out="$(run_complyctl scan --policy-id "${OPA_POLICY_ID}")" || rc=$? + if [[ "${rc}" -ne 0 ]]; then + fail "scan opa: unexpected exit code ${rc}" + echo "${out}" | sanitize_output >&2 + return + fi + echo "${out}" | sanitize_output + assert_contains "scan opa: completed" "${out}" "requirements:" + assert_contains "scan opa: check-run-as-nonroot requirement" "${out}" "check-run-as-nonroot" + assert_contains "scan opa: check-resource-limits requirement" "${out}" "check-resource-limits" + assert_contains "scan opa: failures detected" "${out}" "failed" +} + +# --- test_scan_opa_compliant (compliant fixture — expects all pass) --- + +test_scan_opa_compliant() { + FOUND_FILE="" + echo "" + echo "=== test_scan_opa_compliant ===" + # Swap in the compliant (good) deployment fixture. + cp "${TESTDATA_DIR}/test-deployment-good.yaml" "${WORK_DIR}/test-deployment.yaml" + + local out rc=0 + # Re-generate to pick up fresh state, then scan. + out="$(run_complyctl generate --policy-id "${OPA_POLICY_ID}")" || rc=$? + if [[ "${rc}" -ne 0 ]]; then + fail "scan opa compliant: generate failed with exit code ${rc}" + echo "${out}" | sanitize_output >&2 + return + fi + rc=0 + out="$(run_complyctl scan --policy-id "${OPA_POLICY_ID}")" || rc=$? + if [[ "${rc}" -ne 0 ]]; then + fail "scan opa compliant: unexpected exit code ${rc}" + echo "${out}" | sanitize_output >&2 + return + fi + echo "${out}" | sanitize_output + assert_contains "scan opa compliant: completed" "${out}" "requirements:" + assert_contains "scan opa compliant: all passed" "${out}" "passed" + + # Restore the non-compliant (bad) fixture for any subsequent tests. + cp "${TESTDATA_DIR}/test-deployment-bad.yaml" "${WORK_DIR}/test-deployment.yaml" +} + # --- Run all tests --- test_get test_generate test_scan test_generate_bad_policy +test_get_opa +test_generate_opa +test_scan_opa +test_scan_opa_compliant # --- Summary --- diff --git a/tests/cross-repo/testdata/complytime.yaml b/tests/cross-repo/testdata/complytime.yaml index 552ebfee..db4f003c 100644 --- a/tests/cross-repo/testdata/complytime.yaml +++ b/tests/cross-repo/testdata/complytime.yaml @@ -2,7 +2,7 @@ policies: - url: http://localhost:8765/policies/test-branch-protection id: test-ampel-bp - url: http://localhost:8765/policies/test-opa-policy - id: test-opa-bp + id: test-opa-k8s complypacks: - url: http://localhost:8765/complypacks/ampel-bp@v1.0.0 @@ -19,6 +19,6 @@ targets: specs: builtin:github/branch-rules.yaml - id: test-k8s-deployment policies: - - test-opa-bp + - test-opa-k8s variables: input_path: test-deployment.yaml diff --git a/tests/cross-repo/testdata/test-deployment-bad.yaml b/tests/cross-repo/testdata/test-deployment-bad.yaml new file mode 100644 index 00000000..08effec7 --- /dev/null +++ b/tests/cross-repo/testdata/test-deployment-bad.yaml @@ -0,0 +1,18 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-app + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: test-app + template: + metadata: + labels: + app: test-app + spec: + containers: + - name: web + image: nginx:1.27 diff --git a/tests/cross-repo/testdata/test-deployment-good.yaml b/tests/cross-repo/testdata/test-deployment-good.yaml new file mode 100644 index 00000000..f9d8c65f --- /dev/null +++ b/tests/cross-repo/testdata/test-deployment-good.yaml @@ -0,0 +1,24 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-app + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: test-app + template: + metadata: + labels: + app: test-app + spec: + containers: + - name: web + image: nginx:1.27 + securityContext: + runAsNonRoot: true + resources: + limits: + cpu: "500m" + memory: "128Mi" diff --git a/trivy.yaml b/trivy.yaml new file mode 100644 index 00000000..ac15e785 --- /dev/null +++ b/trivy.yaml @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Apache-2.0 +# Trivy configuration — use path-scoped YAML ignore file +ignorefile: ".trivyignore.yaml"