diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml new file mode 100644 index 0000000..31587e1 --- /dev/null +++ b/.github/workflows/build-binaries.yml @@ -0,0 +1,106 @@ +name: Build Binaries + +on: + workflow_run: + workflows: ["build"] + types: [completed] + branches: [main, develop] + workflow_dispatch: + +permissions: + contents: read + actions: read + checks: read + +env: + GO_VERSION: "1.25.1" + +jobs: + build-binaries: + name: Build Multi-Platform Binaries + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && (github.event.workflow_run.event == 'push' || github.event.workflow_run.event == 'workflow_dispatch')) }} + strategy: + matrix: + include: + - os: linux + arch: amd64 + output: github-repo-linux-amd64 + - os: linux + arch: arm64 + output: github-repo-linux-arm64 + - os: darwin + arch: amd64 + output: github-repo-darwin-amd64 + - os: darwin + arch: arm64 + output: github-repo-darwin-arm64 + - os: windows + arch: amd64 + output: github-repo-windows-amd64.exe + steps: + - name: Validate workflow_run security + if: github.event_name == 'workflow_run' + uses: actions/github-script@v7 + with: + script: | + const workflowRun = context.payload.workflow_run; + + // Validate workflow_run is from the same repository (not a fork) + if (!workflowRun || workflowRun.repository.full_name !== context.repo.owner + '/' + context.repo.repo) { + core.setFailed('Security: workflow_run must be from the same repository'); + return; + } + + // Validate head SHA format + const headSha = workflowRun.head_sha; + if (!headSha || !/^[a-f0-9]{40}$/i.test(headSha)) { + core.setFailed('Invalid head SHA format'); + return; + } + + // Validate branch is allowed + const allowedBranches = ['main', 'develop']; + if (!allowedBranches.includes(workflowRun.head_branch)) { + core.setFailed(`Security: workflow_run from branch '${workflowRun.head_branch}' is not allowed. Allowed branches: ${allowedBranches.join(', ')}`); + return; + } + + // Check if lint workflow passed + const { data: runs } = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'lint.yaml', + head_sha: headSha, + per_page: 1 + }); + if (runs.workflow_runs.length > 0 && runs.workflow_runs[0].conclusion !== 'success') { + core.setFailed('Lint workflow did not pass'); + } + - name: Checkout code + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Build binary for ${{ matrix.os }}/${{ matrix.arch }} + env: + GOOS: ${{ matrix.os }} + GOARCH: ${{ matrix.arch }} + CGO_ENABLED: 0 + VERSION: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || github.event.workflow_run.head_branch || 'unknown' }} + COMMIT_HASH: ${{ github.event_name == 'workflow_dispatch' && github.sha || github.event.workflow_run.head_sha || github.sha }} + OUTPUT_NAME: ${{ matrix.output }} + run: | + go build -ldflags="-s -w -X 'main.Version=${VERSION}' -X 'main.GitCommitHash=${COMMIT_HASH}' -X 'main.BuiltAt=$(date -u +%Y-%m-%dT%H:%M:%SZ)'" -o "${OUTPUT_NAME}" + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.output }} + path: ${{ matrix.output }} + retention-days: 30 diff --git a/.github/workflows/docker-push.yml b/.github/workflows/docker-push.yml new file mode 100644 index 0000000..8d487ec --- /dev/null +++ b/.github/workflows/docker-push.yml @@ -0,0 +1,139 @@ +name: Docker Build and Push + +on: + workflow_run: + workflows: ["build"] + types: [completed] + branches: [main, develop] + workflow_dispatch: + +permissions: + contents: read + packages: write + id-token: write + attestations: write + actions: read + checks: read + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + docker-build-push: + name: Build and Push Docker Image + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && (github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'develop')) }} + steps: + - name: Validate workflow_run security + if: github.event_name == 'workflow_run' + uses: actions/github-script@v7 + with: + script: | + const workflowRun = context.payload.workflow_run; + + // Validate workflow_run is from the same repository (not a fork) + if (!workflowRun || workflowRun.repository.full_name !== context.repo.owner + '/' + context.repo.repo) { + core.setFailed('Security: workflow_run must be from the same repository'); + return; + } + + // Validate head SHA format + const headSha = workflowRun.head_sha; + if (!headSha || !/^[a-f0-9]{40}$/i.test(headSha)) { + core.setFailed('Invalid head SHA format'); + return; + } + + // Validate branch is allowed + const allowedBranches = ['main', 'develop']; + if (!allowedBranches.includes(workflowRun.head_branch)) { + core.setFailed(`Security: workflow_run from branch '${workflowRun.head_branch}' is not allowed. Allowed branches: ${allowedBranches.join(', ')}`); + return; + } + + // Check if lint workflow passed + const { data: runs } = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'lint.yaml', + head_sha: headSha, + per_page: 1 + }); + if (runs.workflow_runs.length > 0 && runs.workflow_runs[0].conclusion !== 'success') { + core.setFailed('Lint workflow did not pass'); + } + - name: Checkout code + uses: actions/checkout@v5 + with: + persist-credentials: false + ref: ${{ github.event_name == 'workflow_dispatch' && github.sha || github.event.workflow_run.head_sha }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Prepare Docker Hub image name + id: dockerhub + run: | + if [ -n "${{ secrets.DOCKERHUB_TOKEN }}" ] && [ -n "${{ secrets.DOCKERHUB_USERNAME }}" ]; then + echo "image=${{ secrets.DOCKERHUB_USERNAME }}/pvtr-github-repo" >> $GITHUB_OUTPUT + echo "has_credentials=true" >> $GITHUB_OUTPUT + else + echo "has_credentials=false" >> $GITHUB_OUTPUT + fi + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + if: steps.dockerhub.outputs.has_credentials == 'true' + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Prepare images list for metadata + id: prepare-images + run: | + echo "images<> $GITHUB_OUTPUT + echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_OUTPUT + if [ "${{ steps.dockerhub.outputs.has_credentials }}" = "true" ]; then + echo "${{ steps.dockerhub.outputs.image }}" >> $GITHUB_OUTPUT + fi + echo "EOF" >> $GITHUB_OUTPUT + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ steps.prepare-images.outputs.images }} + tags: | + type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' || github.event.workflow_run.head_branch == 'main' }} + type=ref,event=branch + type=sha,prefix=${{ github.event_name == 'workflow_dispatch' && github.ref_name || github.event.workflow_run.head_branch }}- + + - name: Build and push Docker image + id: build + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: true + sbom: true + + - name: Attest Build Provenance + uses: actions/attest-build-provenance@v2 + if: ${{ github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' || github.event.workflow_run.head_branch == 'main' }} + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 0000000..7ec532b --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,93 @@ +name: Integration Test + +on: + workflow_run: + workflows: ["Docker Build and Push"] + types: [completed] + branches: [main] + workflow_dispatch: + +permissions: + contents: read + packages: read + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + integration-test: + name: Integration Test + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }} + steps: + - name: Validate workflow_run security + if: github.event_name == 'workflow_run' + uses: actions/github-script@v7 + with: + script: | + const workflowRun = context.payload.workflow_run; + + // Validate workflow_run is from the same repository (not a fork) + if (!workflowRun || workflowRun.repository.full_name !== context.repo.owner + '/' + context.repo.repo) { + core.setFailed('Security: workflow_run must be from the same repository'); + return; + } + + // Validate head SHA format + const headSha = workflowRun.head_sha; + if (!headSha || !/^[a-f0-9]{40}$/i.test(headSha)) { + core.setFailed('Invalid head SHA format'); + return; + } + + // Validate branch is allowed + const allowedBranches = ['main']; + if (!allowedBranches.includes(workflowRun.head_branch)) { + core.setFailed(`Security: workflow_run from branch '${workflowRun.head_branch}' is not allowed. Allowed branches: ${allowedBranches.join(', ')}`); + return; + } + - name: Checkout code + uses: actions/checkout@v5 + with: + persist-credentials: false + ref: ${{ github.event_name == 'workflow_dispatch' && github.sha || github.event.workflow_run.head_sha }} + + - name: Create test config + env: + REPO_OWNER: ${{ github.repository_owner }} + REPO_NAME: ${{ github.event.repository.name }} + run: | + cat > test-config.yml << EOF + loglevel: info + write-directory: evaluation_results + write: true + output: yaml + services: + self-test: + plugin: github-repo + policy: + catalogs: + - OSPS_B + applicability: + - Maturity Level 1 + vars: + owner: ${REPO_OWNER} + repo: ${REPO_NAME} + token: \${{ secrets.GITHUB_TOKEN }} + EOF + + - name: Run self-assessment using Docker image + run: | + docker run --rm \ + -v $(pwd)/test-config.yml:/.privateer/config.yml \ + -v $(pwd)/evaluation_results:/evaluation_results \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Upload evaluation results + uses: actions/upload-artifact@v4 + if: always() + with: + name: evaluation-results + path: evaluation_results/ + retention-days: 30 diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml new file mode 100644 index 0000000..6b0b99f --- /dev/null +++ b/.github/workflows/security-scan.yml @@ -0,0 +1,89 @@ +name: Security Scan + +on: + workflow_run: + workflows: ["build"] + types: [completed] + branches: [main, develop] + workflow_dispatch: + +permissions: + security-events: write + contents: read + actions: read + checks: read + +jobs: + security-scan: + name: Security Scan + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} + steps: + - name: Validate workflow_run security + if: github.event_name == 'workflow_run' + uses: actions/github-script@v7 + with: + script: | + const workflowRun = context.payload.workflow_run; + + // Validate workflow_run is from the same repository (not a fork) + if (!workflowRun || workflowRun.repository.full_name !== context.repo.owner + '/' + context.repo.repo) { + core.setFailed('Security: workflow_run must be from the same repository'); + return; + } + + // Validate head SHA format + const headSha = workflowRun.head_sha; + if (!headSha || !/^[a-f0-9]{40}$/i.test(headSha)) { + core.setFailed('Invalid head SHA format'); + return; + } + + // Validate branch is allowed + const allowedBranches = ['main', 'develop']; + if (!allowedBranches.includes(workflowRun.head_branch)) { + core.setFailed(`Security: workflow_run from branch '${workflowRun.head_branch}' is not allowed. Allowed branches: ${allowedBranches.join(', ')}`); + return; + } + + // Check if lint workflow passed + const { data: runs } = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'lint.yaml', + head_sha: headSha, + per_page: 1 + }); + if (runs.workflow_runs.length > 0 && runs.workflow_runs[0].conclusion !== 'success') { + core.setFailed('Lint workflow did not pass'); + } + - name: Checkout code + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + scan-type: "fs" + scan-ref: "." + format: "sarif" + output: "trivy-results.sarif" + severity: "CRITICAL,HIGH" + + - name: Upload Trivy results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: "trivy-results.sarif" + + - name: Run Gosec Security Scanner + uses: securego/gosec@v2.21.4 + with: + args: "-no-fail -fmt sarif -out gosec-results.sarif ./..." + + - name: Upload Gosec results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: "gosec-results.sarif" diff --git a/README.md b/README.md index 73d150a..01f91d2 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,9 @@ docker run \ ## GitHub Actions Usage -We've pushed an image to docker hub for use in GitHub Actions. +We've pushed images to GitHub Container Registry and Docker Hub for use in GitHub Actions. -You will also need to set up a GitHub personal access token with the repository read permissions. This token should be added to your config file, or — if using the example pipeline below — as a secret in your repository. +You will need a GitHub personal access token with repository read permissions. This token should be added to your config file or as a secret in your repository. ### Example GHA Setup @@ -35,6 +35,12 @@ You will also need to set up a GitHub personal access token with the repository - [Workflow Definition](https://github.com/privateerproj/.github/blob/main/.github/workflows/osps-baseline.yml) - [Action Results](https://github.com/privateerproj/.github/actions/runs/13691384519/job/38285134201) +### CI/CD Pipeline + +This repository uses GitHub Actions for continuous integration and deployment. The workflow automatically builds, tests, performs security scanning, and publishes multi-platform binaries and Docker images. + +For Docker Hub publishing, add `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` secrets to your repository settings. + ## Contributing Contributions are welcome! Please see our [Contributing Guidelines](.github/CONTRIBUTING.md) for more information.