From 18cb3aa7ed208f533d2ff89345daabc3aeb762e4 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Sun, 8 Feb 2026 09:29:33 -0500 Subject: [PATCH] feat: Add Gitea Actions workflows for validation and security --- .checkov.yaml | 8 + .gitea/workflows/best-practices.yaml | 163 +++++++++++++++ .gitea/workflows/security.yaml | 302 +++++++++++++++++++++++++++ .gitea/workflows/validate.yaml | 113 ++++++++++ .trivyignore | 5 + .yamllint.yaml | 6 + 6 files changed, 597 insertions(+) create mode 100644 .checkov.yaml create mode 100644 .gitea/workflows/best-practices.yaml create mode 100644 .gitea/workflows/security.yaml create mode 100644 .gitea/workflows/validate.yaml create mode 100644 .trivyignore create mode 100644 .yamllint.yaml diff --git a/.checkov.yaml b/.checkov.yaml new file mode 100644 index 0000000..cdb21df --- /dev/null +++ b/.checkov.yaml @@ -0,0 +1,8 @@ +soft-fail: false +quiet: true +compact: true +framework: + - all +skip-check: + - CKV_K8S_21 # Default namespace usage + - CKV_K8S_43 # Image tag validation diff --git a/.gitea/workflows/best-practices.yaml b/.gitea/workflows/best-practices.yaml new file mode 100644 index 0000000..96da358 --- /dev/null +++ b/.gitea/workflows/best-practices.yaml @@ -0,0 +1,163 @@ +name: Best Practices + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + kube-score: + name: Kube-score Analysis + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install kubectl and kube-score + run: | + # Install kubectl + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + # Install kube-score + wget https://github.com/zegl/kube-score/releases/download/v1.18.0/kube-score_1.18.0_linux_amd64.tar.gz + tar -xzf kube-score_1.18.0_linux_amd64.tar.gz + chmod +x kube-score + mv kube-score /usr/local/bin/ + + - name: Run kube-score + run: | + if [ -f "kustomization.yaml" ]; then + kubectl kustomize . | kube-score score - \ + --ignore-test pod-networkpolicy \ + --ignore-test deployment-has-poddisruptionbudget \ + --ignore-test container-security-context-user-group-id \ + --ignore-test container-security-context-readonlyrootfilesystem \ + --output-format ci + fi + + polaris: + name: Polaris Audit + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install kubectl and polaris + run: | + # Install kubectl + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + # Install polaris + wget https://github.com/FairwindsOps/polaris/releases/download/9.5.0/polaris_linux_amd64.tar.gz + tar -xzf polaris_linux_amd64.tar.gz + chmod +x polaris + mv polaris /usr/local/bin/ + + - name: Run Polaris audit + run: | + if [ -f "kustomization.yaml" ]; then + kubectl kustomize . > manifests.yaml + polaris audit --audit-path manifests.yaml \ + --format pretty \ + --set-exit-code-on-danger \ + --set-exit-code-below-score 70 + fi + + resource-analysis: + name: Resource Usage Analysis + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install kubectl and yq + run: | + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O yq + chmod +x yq + mv yq /usr/local/bin/ + + - name: Analyze resource requests and limits + run: | + echo "# Resource Analysis Report" + echo "" + echo "## Applications Resource Configuration" + echo "" + echo "| Application | Container | CPU Request | CPU Limit | Memory Request | Memory Limit |" + echo "|-------------|-----------|-------------|-----------|----------------|--------------|" + + # Find all directories with kustomization.yaml + find . -maxdepth 2 -name "kustomization.yaml" | while read config; do + app_dir=$(dirname "$config") + if [ "$app_dir" != "." ]; then + manifests=$(kubectl kustomize "$app_dir" 2>/dev/null) + if [ -n "$manifests" ]; then + echo "$manifests" | yq eval-all ' + select(.kind == "Deployment" or .kind == "StatefulSet") | + .spec.template.spec.containers[] | + "| '"$app_dir"' | \(.name) | \(.resources.requests.cpu // "none") | \(.resources.limits.cpu // "none") | \(.resources.requests.memory // "none") | \(.resources.limits.memory // "none") |" + ' - 2>/dev/null || true + fi + fi + done + + pr-summary: + name: PR Summary Report + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + needs: [kube-score, polaris, resource-analysis] + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Generate PR summary + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments + run: | + cat > summary.md << EOF + ## Best Practices Validation Summary + + ✅ All validation checks completed + + ### Checks Run: + - **kube-score**: Kubernetes best practices analysis + - **Polaris**: Security and reliability audit + - **Resource Analysis**: CPU and memory configuration review + + See individual job logs for detailed results. + + --- + *Automated by Gitea Actions* + EOF + + if [ -n "${GITEA_TOKEN}" ]; then + jq -n --rawfile body summary.md '{body: $body}' > comment-payload.json + + curl -s -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d @comment-payload.json \ + "${GITEA_API}" || echo "Failed to post comment (token may not be configured)" + else + echo "GITEA_TOKEN not configured, skipping comment" + cat summary.md + fi diff --git a/.gitea/workflows/security.yaml b/.gitea/workflows/security.yaml new file mode 100644 index 0000000..4399bda --- /dev/null +++ b/.gitea/workflows/security.yaml @@ -0,0 +1,302 @@ +name: Security Scan + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + trivy-scan: + name: Trivy Security Scan + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Trivy + run: | + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + + - name: Run Trivy config scan + run: | + trivy config \ + --severity CRITICAL,HIGH \ + --ignorefile .trivyignore \ + --exit-code 0 \ + --format table \ + . + + trivy-pr-review: + name: Trivy PR Review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Trivy and jq + run: | + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + apt-get update && apt-get install -y jq + + - name: Get changed files + id: changed + run: | + CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- '*.yaml' '*.yml' '*.tf' | tr '\n' ' ') + echo "files=${CHANGED_FILES}" >> $GITHUB_OUTPUT + echo "Changed files: ${CHANGED_FILES}" + + - name: Run Trivy scan and post review + env: + GITEA_TOKEN: ${{ secrets.TRIVY_GITEA_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews + CHANGED_FILES: ${{ steps.changed.outputs.files }} + run: | + if [ -z "${CHANGED_FILES}" ]; then + echo "No IaC files changed, skipping scan" + exit 0 + fi + + trivy config \ + --severity CRITICAL,HIGH,MEDIUM,LOW \ + --ignorefile .trivyignore \ + --exit-code 0 \ + --format json \ + --output trivy-results.json \ + . + + CHANGED_FILES_JSON=$(echo "${CHANGED_FILES}" | tr ' ' '\n' | sed '/^$/d' | jq -R . | jq -s .) + + CRITICAL=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.Results[]? | select(.Target as $t | $files | any(. as $f | $t | endswith($f))) | .Misconfigurations[]? | select(.Severity == "CRITICAL")] | length' \ + trivy-results.json) + HIGH=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.Results[]? | select(.Target as $t | $files | any(. as $f | $t | endswith($f))) | .Misconfigurations[]? | select(.Severity == "HIGH")] | length' \ + trivy-results.json) + MEDIUM=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.Results[]? | select(.Target as $t | $files | any(. as $f | $t | endswith($f))) | .Misconfigurations[]? | select(.Severity == "MEDIUM")] | length' \ + trivy-results.json) + LOW=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.Results[]? | select(.Target as $t | $files | any(. as $f | $t | endswith($f))) | .Misconfigurations[]? | select(.Severity == "LOW")] | length' \ + trivy-results.json) + TOTAL_FAILED=$(( CRITICAL + HIGH + MEDIUM + LOW )) + + if [ "$CRITICAL" -gt 0 ]; then + REVIEW_STATE="REQUEST_CHANGES" + VERDICT="BLOCKED: ${CRITICAL} critical finding(s) detected" + EXIT_CODE=1 + elif [ "$HIGH" -gt 0 ]; then + REVIEW_STATE="COMMENT" + VERDICT="WARNING: ${HIGH} high severity finding(s) detected" + EXIT_CODE=0 + else + REVIEW_STATE="APPROVED" + VERDICT="PASSED: No critical or high severity findings" + EXIT_CODE=0 + fi + + DETAILS=$(jq -r --argjson files "$CHANGED_FILES_JSON" ' + [.Results[]? | select(.Target as $t | $files | any(. as $f | $t | endswith($f))) | + .Target as $t | .Misconfigurations[]? | + "| \(.Severity) | \(.ID) | \(.Title) | \($t) |" + ] | join("\n")' trivy-results.json) + + cat > review-body.md << EOF + ## Trivy Security Scan Results + + **${VERDICT}** + + > Scanned ${CHANGED_FILES:-"no files"} + + ### Summary + | Severity | Count | + |----------|-------| + | Critical | ${CRITICAL} | + | High | ${HIGH} | + | Medium | ${MEDIUM} | + | Low | ${LOW} | + | **Total** | **${TOTAL_FAILED}** | + +
+ Failed Checks (click to expand) + + | Severity | Check ID | Description | File | + |----------|----------|-------------|------| + ${DETAILS} + +
+ + --- + *Scanned by [Trivy](https://github.com/aquasecurity/trivy)* + EOF + + jq -n \ + --rawfile body review-body.md \ + --arg event "$REVIEW_STATE" \ + '{body: $body, event: $event}' > review-payload.json + + echo "Posting review to: ${GITEA_API}" + echo "Review state: ${REVIEW_STATE}" + + curl -s -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d @review-payload.json \ + "${GITEA_API}" || echo "Failed to post review (token may not be configured)" + + exit $EXIT_CODE + + checkov-scan: + name: Checkov IaC Scan + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Checkov + run: | + python3 -m venv /opt/checkov + /opt/checkov/bin/pip install checkov + + - name: Run Checkov scan + run: | + /opt/checkov/bin/checkov -d . \ + --config-file .checkov.yaml \ + --output cli + + checkov-pr-review: + name: Checkov PR Review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Checkov and jq + run: | + python3 -m venv /opt/checkov + /opt/checkov/bin/pip install checkov + apt-get update && apt-get install -y jq + + - name: Get changed files + id: changed + run: | + CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- '*.yaml' '*.yml' '*.tf' | tr '\n' ' ') + echo "files=${CHANGED_FILES}" >> $GITHUB_OUTPUT + echo "Changed files: ${CHANGED_FILES}" + + - name: Run Checkov scan and post review + env: + GITEA_TOKEN: ${{ secrets.CHECKOV_GITEA_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews + CHANGED_FILES: ${{ steps.changed.outputs.files }} + run: | + if [ -z "${CHANGED_FILES}" ]; then + echo "No IaC files changed, skipping scan" + exit 0 + fi + + /opt/checkov/bin/checkov -d . \ + --config-file .checkov.yaml \ + --output json \ + > checkov-results.json || true + + CHANGED_FILES_JSON=$(echo "${CHANGED_FILES}" | tr ' ' '\n' | sed '/^$/d' | jq -R . | jq -s .) + + CRITICAL=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.[] | .results.failed_checks[]? | select(.file_path as $fp | $files | any(. as $f | $fp | endswith($f))) | select(.severity == "CRITICAL")] | length' \ + checkov-results.json) + HIGH=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.[] | .results.failed_checks[]? | select(.file_path as $fp | $files | any(. as $f | $fp | endswith($f))) | select(.severity == "HIGH")] | length' \ + checkov-results.json) + MEDIUM=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.[] | .results.failed_checks[]? | select(.file_path as $fp | $files | any(. as $f | $fp | endswith($f))) | select(.severity == "MEDIUM")] | length' \ + checkov-results.json) + LOW=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.[] | .results.failed_checks[]? | select(.file_path as $fp | $files | any(. as $f | $fp | endswith($f))) | select(.severity == "LOW")] | length' \ + checkov-results.json) + UNSET=$(jq --argjson files "$CHANGED_FILES_JSON" \ + '[.[] | .results.failed_checks[]? | select(.file_path as $fp | $files | any(. as $f | $fp | endswith($f))) | select(.severity == null or .severity == "UNKNOWN" or .severity == "NONE")] | length' \ + checkov-results.json) + TOTAL_FAILED=$(( CRITICAL + HIGH + MEDIUM + LOW + UNSET )) + + if [ "$CRITICAL" -gt 0 ]; then + REVIEW_STATE="REQUEST_CHANGES" + VERDICT="BLOCKED: ${CRITICAL} critical finding(s) detected" + EXIT_CODE=1 + elif [ "$HIGH" -gt 0 ]; then + REVIEW_STATE="COMMENT" + VERDICT="WARNING: ${HIGH} high severity finding(s) detected" + EXIT_CODE=0 + else + REVIEW_STATE="APPROVED" + VERDICT="PASSED: No critical or high severity findings" + EXIT_CODE=0 + fi + + DETAILS=$(jq -r --argjson files "$CHANGED_FILES_JSON" ' + [.[] | .check_type as $ct | + .results.failed_checks[]? | + select(.file_path as $fp | $files | any(. as $f | $fp | endswith($f))) | + "| \(.severity // "UNSET") | \(.check_id) | \(.check_name) | \(.file_path) |" + ] | join("\n")' checkov-results.json) + + cat > review-body.md << EOF + ## Checkov IaC Scan Results + + **${VERDICT}** + + > Scanned ${CHANGED_FILES:-"no files"} + + ### Summary + | Severity | Count | + |----------|-------| + | Critical | ${CRITICAL} | + | High | ${HIGH} | + | Medium | ${MEDIUM} | + | Low | ${LOW} | + | Unset | ${UNSET} | + | **Total** | **${TOTAL_FAILED}** | + +
+ Failed Checks (click to expand) + + | Severity | Check ID | Description | File | + |----------|----------|-------------|------| + ${DETAILS} + +
+ + --- + *Scanned by [Checkov](https://github.com/bridgecrewio/checkov)* + EOF + + jq -n \ + --rawfile body review-body.md \ + --arg event "$REVIEW_STATE" \ + '{body: $body, event: $event}' > review-payload.json + + curl -s -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d @review-payload.json \ + "${GITEA_API}" || echo "Failed to post review (token may not be configured)" + + exit $EXIT_CODE diff --git a/.gitea/workflows/validate.yaml b/.gitea/workflows/validate.yaml new file mode 100644 index 0000000..118428a --- /dev/null +++ b/.gitea/workflows/validate.yaml @@ -0,0 +1,113 @@ +name: Validate Manifests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + yaml-lint: + name: YAML Lint + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install yamllint + run: | + python3 -m pip install yamllint + + - name: Run yamllint + run: | + yamllint -c .yamllint.yaml . + + kustomize-build: + name: Kustomize Build Test + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install kubectl with kustomize + run: | + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + - name: Test root kustomization + run: | + if [ -f "kustomization.yaml" ]; then + echo "Building root kustomization..." + kubectl kustomize . > /tmp/manifests.yaml + echo "✓ Root kustomization builds successfully" + else + echo "No root kustomization.yaml found" + fi + + - name: Test individual app kustomizations + run: | + find . -maxdepth 2 -name "kustomization.yaml" -not -path "./kustomization.yaml" | while read config; do + app_dir=$(dirname "$config") + echo "Building $app_dir kustomization..." + kubectl kustomize "$app_dir" > /dev/null + echo "✓ $app_dir kustomization builds successfully" + done + + kubeconform: + name: Kubernetes Schema Validation + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install kubectl and kubeconform + run: | + # Install kubectl + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + # Install kubeconform + curl -L https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz | tar xz + chmod +x kubeconform + mv kubeconform /usr/local/bin/ + + - name: Validate Kubernetes manifests + run: | + if [ -f "kustomization.yaml" ]; then + kubectl kustomize . | kubeconform \ + -schema-location default \ + -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \ + -summary \ + -output text \ + -ignore-missing-schemas \ + -verbose + fi + + flux-validate: + name: Flux Build Validation + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Flux CLI + run: | + curl -s https://fluxcd.io/install.sh | bash + mv /root/.local/bin/flux /usr/local/bin/ + + - name: Validate Flux Kustomization + run: | + # Use the repository name or 'app' as the kustomization name for validation + flux build kustomization irc --path . --dry-run diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 0000000..95b2311 --- /dev/null +++ b/.trivyignore @@ -0,0 +1,5 @@ +# Avahi daemon in base images often triggers this +CVE-2021-26720 + +# Common false positives or accepted risks +CVE-2023-52425 diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 0000000..33b82f9 --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,6 @@ +extends: default + +rules: + line-length: disable + document-start: disable + truthy: disable