diff --git a/CODEOWNERS b/CODEOWNERS index 7e1a8b50f8..7c0d85bdf2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -128,6 +128,9 @@ # renovate groupName=oci-run-script /task/run-script-oci-ta @konflux-ci/build-maintainers @Zokormazo @arewm +# renovate groupName=verify-source +/task/verify-source @arewm @ralphbean + # These are auto-generated and often require changes when tasks change. # Allow anyone with write access to approve the changes. /pipelines/*/README.md diff --git a/renovate.json b/renovate.json index 55acd43237..a98b1ed017 100644 --- a/renovate.json +++ b/renovate.json @@ -240,6 +240,12 @@ "task/run-script-oci-ta/**" ] }, + { + "groupName": "verify-source", + "matchFileNames": [ + "task/verify-source/**" + ] + }, { "groupName": "mobster", "matchFileNames": [ diff --git a/task/verify-source/0.1/README.md b/task/verify-source/0.1/README.md new file mode 100644 index 0000000000..7ed2783c20 --- /dev/null +++ b/task/verify-source/0.1/README.md @@ -0,0 +1,71 @@ +# verify-source task + +The verify-source Task verifies the SLSA source level of a git commit +by checking for a Verification Summary Attestation (VSA) stored as a +git note. The task fetches git notes from the repository and parses +the VSA to extract the verified SLSA source level. + +WARNING: This task relies on VSAs generated by source-tool +(https://github.com/slsa-framework/source-tool) which is currently a +proof-of-concept and under active development. It should not be used in +production environments. It supports GitHub and GitLab repositories, +and may encounter API rate limits without authentication. + + +## Parameters +|name|description|default value|required| +|---|---|---|---| +|url|Repository URL to verify.||true| +|revision|Commit SHA to verify.||true| + +## Results +|name|description| +|---|---| +|SLSA_SOURCE_LEVEL_ACHIEVED|The SLSA source level achieved by this commit| +|TEST_OUTPUT|JSON formatted test results for SLSA verification| + +## Workspaces +|name|description|optional| +|---|---|---| +|basic-auth|A Workspace containing a token file for API authentication. The workspace should contain a file named 'token' with a GitHub personal access token, GitLab personal access token, or other authentication token. The task will automatically set the appropriate environment variable (GITHUB_TOKEN or GITLAB_TOKEN) based on the repository host. This is used to avoid rate limiting when accessing the API. Binding a Secret to this Workspace is strongly recommended over other volume types. |true| + +## Additional info + +### API Authentication + +To avoid API rate limits, you can provide an authentication token via the `basic-auth` workspace. The task automatically detects the repository host and sets the appropriate environment variable (`GITHUB_TOKEN` for GitHub, `GITLAB_TOKEN` for GitLab). + +**Create a secret with your token:** + +```bash +# For GitHub +kubectl create secret generic git-token \ + --from-literal=token=ghp_yourGitHubTokenHere + +# For GitLab +kubectl create secret generic git-token \ + --from-literal=token=glpat-yourGitLabTokenHere +``` + +**Use the secret in your pipeline:** + +```yaml +apiVersion: tekton.dev/v1 +kind: TaskRun +metadata: + name: verify-source-example +spec: + taskRef: + name: verify-source + params: + - name: url + value: https://github.com/slsa-framework/source-tool + - name: revision + value: 134593d9158efd253e979e2e8d87b939945d091e + workspaces: + - name: basic-auth + secret: + secretName: git-token +``` + +The task will automatically detect the `token` file in the workspace and set the appropriate environment variable based on the repository URL. diff --git a/task/verify-source/0.1/tests/test-verify-source-no-vsa.yaml b/task/verify-source/0.1/tests/test-verify-source-no-vsa.yaml new file mode 100644 index 0000000000..1990d84bef --- /dev/null +++ b/task/verify-source/0.1/tests/test-verify-source-no-vsa.yaml @@ -0,0 +1,68 @@ +--- +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: test-verify-source-no-vsa +spec: + description: | + Test the verify-source task with a repository that has no VSA + tasks: + - name: run-task + taskRef: + name: verify-source + params: + - name: url + value: https://github.com/kelseyhightower/nocode + - name: revision + value: ed6c73fc16578ec53ea374585df2b965ce9f4a31 + - name: check-result + params: + - name: SLSA_SOURCE_LEVEL_ACHIEVED + value: $(tasks.run-task.results.SLSA_SOURCE_LEVEL_ACHIEVED) + - name: TEST_OUTPUT + value: $(tasks.run-task.results.TEST_OUTPUT) + taskSpec: + params: + - name: SLSA_SOURCE_LEVEL_ACHIEVED + - name: TEST_OUTPUT + steps: + - name: check-result + env: + - name: SLSA_LEVEL_ACHIEVED + value: $(params.SLSA_SOURCE_LEVEL_ACHIEVED) + - name: TEST_OUTPUT + value: $(params.TEST_OUTPUT) + image: quay.io/konflux-ci/appstudio-utils:1610c1fc4cfc9c9053dbefc1146904a4df6659ef@sha256:90ac97b811073cb99a23232c15a08082b586c702b85da6200cf54ef505e3c50c + script: | + #!/usr/bin/env sh + set -eux + + # Strip trailing newlines from results + LEVEL=$(echo "$SLSA_LEVEL_ACHIEVED" | tr -d '\n') + OUTPUT=$(echo "$TEST_OUTPUT" | tr -d '\n') + + echo "SLSA_SOURCE_LEVEL_ACHIEVED: $LEVEL" + echo "TEST_OUTPUT: $OUTPUT" + + # For repos without VSA, verify SLSA level is 1 (baseline) + if [ "$LEVEL" != "SLSA_SOURCE_LEVEL_1" ]; then + echo "ERROR: Expected SLSA_SOURCE_LEVEL_1 for repo without VSA, got: $LEVEL" + exit 1 + fi + + # Verify that TEST_OUTPUT is non-empty + if [ -z "$OUTPUT" ]; then + echo "ERROR: TEST_OUTPUT is empty" + exit 1 + fi + + # Verify that there is at least one warning (no VSA found) + WARNINGS=$(echo "$OUTPUT" | grep -o '"warnings": [0-9]*' | grep -o '[0-9]*' || echo "0") + if [ "$WARNINGS" -eq 0 ]; then + echo "ERROR: Expected at least one warning for repo without VSA" + exit 1 + fi + + echo "SUCCESS: No-VSA verification test passed - correctly identified repo without VSA" + runAfter: + - run-task diff --git a/task/verify-source/0.1/tests/test-verify-source-with-vsa.yaml b/task/verify-source/0.1/tests/test-verify-source-with-vsa.yaml new file mode 100644 index 0000000000..f54b0a39c4 --- /dev/null +++ b/task/verify-source/0.1/tests/test-verify-source-with-vsa.yaml @@ -0,0 +1,61 @@ +--- +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: test-verify-source-with-vsa +spec: + description: | + Test the verify-source task with SLSA source verification + tasks: + - name: run-task + taskRef: + name: verify-source + params: + - name: url + value: https://github.com/slsa-framework/source-tool + - name: revision + value: 134593d9158efd253e979e2e8d87b939945d091e + - name: check-result + params: + - name: SLSA_SOURCE_LEVEL_ACHIEVED + value: $(tasks.run-task.results.SLSA_SOURCE_LEVEL_ACHIEVED) + - name: TEST_OUTPUT + value: $(tasks.run-task.results.TEST_OUTPUT) + taskSpec: + params: + - name: SLSA_SOURCE_LEVEL_ACHIEVED + - name: TEST_OUTPUT + steps: + - name: check-result + env: + - name: SLSA_LEVEL_ACHIEVED + value: $(params.SLSA_SOURCE_LEVEL_ACHIEVED) + - name: TEST_OUTPUT + value: $(params.TEST_OUTPUT) + image: quay.io/konflux-ci/appstudio-utils:1610c1fc4cfc9c9053dbefc1146904a4df6659ef@sha256:90ac97b811073cb99a23232c15a08082b586c702b85da6200cf54ef505e3c50c + script: | + #!/usr/bin/env sh + set -eux + + # Strip trailing newlines from results + LEVEL=$(echo "$SLSA_LEVEL_ACHIEVED" | tr -d '\n') + OUTPUT=$(echo "$TEST_OUTPUT" | tr -d '\n') + + echo "SLSA_SOURCE_LEVEL_ACHIEVED: $LEVEL" + echo "TEST_OUTPUT: $OUTPUT" + + # Verify that SLSA level is 3 (extracted from VSA) + if [ "$LEVEL" != "SLSA_SOURCE_LEVEL_3" ]; then + echo "ERROR: Expected SLSA_SOURCE_LEVEL_3 from slsa-framework/source-tool repo VSA, got: $LEVEL" + exit 1 + fi + + # Verify that TEST_OUTPUT is non-empty + if [ -z "$OUTPUT" ]; then + echo "ERROR: TEST_OUTPUT is empty" + exit 1 + fi + + echo "SUCCESS: SLSA verification test passed" + runAfter: + - run-task diff --git a/task/verify-source/0.1/verify-source.yaml b/task/verify-source/0.1/verify-source.yaml new file mode 100644 index 0000000000..ffdf7e0797 --- /dev/null +++ b/task/verify-source/0.1/verify-source.yaml @@ -0,0 +1,187 @@ +--- +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: verify-source + annotations: + tekton.dev/pipelines.minVersion: 0.21.0 + tekton.dev/tags: security, slsa + labels: + app.kubernetes.io/version: "0.1" +spec: + description: | + The verify-source Task verifies the SLSA source level of a git commit + by checking for a Verification Summary Attestation (VSA) stored as a + git note. The task fetches git notes from the repository and parses + the VSA to extract the verified SLSA source level. + + WARNING: This task relies on VSAs generated by source-tool + (https://github.com/slsa-framework/source-tool) which is currently a + proof-of-concept and under active development. It should not be used in + production environments. It supports GitHub and GitLab repositories, + and may encounter API rate limits without authentication. + params: + - name: url + description: Repository URL to verify. + type: string + - name: revision + description: Commit SHA to verify. + type: string + results: + - name: SLSA_SOURCE_LEVEL_ACHIEVED + description: The SLSA source level achieved by this commit + - name: TEST_OUTPUT + description: JSON formatted test results for SLSA verification + workspaces: + - name: basic-auth + description: | + A Workspace containing a token file for API authentication. + The workspace should contain a file named 'token' with a GitHub + personal access token, GitLab personal access token, or other + authentication token. The task will automatically set the appropriate + environment variable (GITHUB_TOKEN or GITLAB_TOKEN) based on the + repository host. This is used to avoid rate limiting when accessing + the API. Binding a Secret to this Workspace is strongly recommended + over other volume types. + optional: true + steps: + - name: slsa-verify + image: quay.io/konflux-ci/git-clone@sha256:bd303d16e9d9b01622d69deff77c583ebdea36611b15dc243da658d93763e8de + env: + - name: PARAM_URL + value: $(params.url) + - name: PARAM_REVISION + value: $(params.revision) + - name: WORKSPACE_BASIC_AUTH_DIRECTORY_BOUND + value: $(workspaces.basic-auth.bound) + - name: WORKSPACE_BASIC_AUTH_DIRECTORY_PATH + value: $(workspaces.basic-auth.path) + script: | + #!/usr/bin/env bash + set -euo pipefail + + # Initialize variables + REPO_URL="${PARAM_URL}" + COMMIT_SHA="${PARAM_REVISION}" + + # Extract repository information + REPO_HOST=$(echo "$REPO_URL" | awk -F/ '{print $3}') + REPO_OWNER=$(echo "$REPO_URL" | awk -F/ '{print $4}') + REPO_NAME=$(echo "$REPO_URL" | awk -F/ '{print $5}' | sed 's/\.git$//') + + # Check for token in workspace and set appropriate environment variable + if [ "${WORKSPACE_BASIC_AUTH_DIRECTORY_BOUND}" = "true" ]; then + if [ -f "${WORKSPACE_BASIC_AUTH_DIRECTORY_PATH}/token" ]; then + TOKEN=$(cat "${WORKSPACE_BASIC_AUTH_DIRECTORY_PATH}/token") + + # Set appropriate environment variable based on repo host + case "$REPO_HOST" in + *github.com) + export GITHUB_TOKEN="$TOKEN" + echo "Using GitHub token from workspace for authentication" + ;; + *gitlab.com|*gitlab.*) + export GITLAB_TOKEN="$TOKEN" + echo "Using GitLab token from workspace for authentication" + ;; + *) + echo "Warning: Unknown host $REPO_HOST, token authentication may not work" + ;; + esac + fi + fi + + echo "=== SLSA Source Verification ===" + echo "Repository: $REPO_HOST/$REPO_OWNER/$REPO_NAME" + echo "Commit: $COMMIT_SHA" + + # Initialize test results + VERIFICATION_RESULT="PASSED" + ACHIEVED_LEVEL="SLSA_SOURCE_LEVEL_1" + SUCCESSES=0 + FAILURES=0 + WARNINGS=0 + TESTS=() + + # Attempt to fetch and parse VSA from git notes + echo "Running SLSA source verification..." + + # Create a temporary directory for git operations + TEMP_DIR=$(mktemp -d) + cd "$TEMP_DIR" + git init --bare repo.git + cd repo.git + + # Fetch the git notes containing VSAs (only notes, not the entire repository) + if ! git fetch "$REPO_URL" 'refs/notes/commits:refs/notes/commits' 2>&1; then + echo "Failed to fetch git notes from repository" + WARNINGS=$((WARNINGS + 1)) + TESTS+=('{"name":"vsa-fetch","result":"WARNING"}') + cd / && rm -rf "$TEMP_DIR" + else + echo "Successfully fetched git notes from repository" + + # Try to get the VSA for this commit + VSA_NOTE=$(git notes --ref=commits show "$COMMIT_SHA" 2>/dev/null || echo "") + + if [ -z "$VSA_NOTE" ]; then + echo "No VSA found in git notes for this commit" + WARNINGS=$((WARNINGS + 1)) + TESTS+=('{"name":"vsa-fetch","result":"WARNING"}') + else + echo "VSA found in git notes" + SUCCESSES=$((SUCCESSES + 1)) + TESTS+=('{"name":"vsa-fetch","result":"PASSED"}') + + # Parse the VSA to extract the SLSA level + # VSA note contains two JSON objects - the second one has the actual VSA + # Extract and decode the payload, then parse verifiedLevels field + VSA_JSON=$(echo "$VSA_NOTE" | tail -1) + PAYLOAD_B64=$(echo "$VSA_JSON" | grep -o '"payload":"[^"]*"' | sed 's/"payload":"//;s/"$//') + DECODED_PAYLOAD=$(echo "$PAYLOAD_B64" | base64 -d 2>/dev/null || echo "") + EXTRACTED_LEVEL=$(echo "$DECODED_PAYLOAD" | \ + grep -o '"verifiedLevels":\["[^"]*"' | \ + sed 's/"verifiedLevels":\["\([^"]*\)"/\1/') + + if [ -n "$EXTRACTED_LEVEL" ]; then + ACHIEVED_LEVEL="$EXTRACTED_LEVEL" + echo "Extracted SLSA level from VSA: $ACHIEVED_LEVEL" + else + echo "Could not parse SLSA level from VSA, using default: $ACHIEVED_LEVEL" + fi + fi + + # Clean up temp directory + cd / && rm -rf "$TEMP_DIR" + fi + + SUCCESSES=$((SUCCESSES + 1)) + TESTS+=('{"name":"slsa-level-determination","result":"PASSED"}') + + # Generate test output JSON + TESTS_JSON=$(printf '%s\n' "${TESTS[@]}" | paste -sd ',' -) + + TEST_OUTPUT=$( + cat <"$(results.SLSA_SOURCE_LEVEL_ACHIEVED.path)" + echo "$TEST_OUTPUT" >"$(results.TEST_OUTPUT.path)" + + echo "=== SLSA Verification Summary ===" + echo "Result: $VERIFICATION_RESULT" + echo "Achieved Level: $ACHIEVED_LEVEL" + echo "Successes: $SUCCESSES, Failures: $FAILURES, Warnings: $WARNINGS" + securityContext: + runAsUser: 0