diff --git a/.gitea/workflows/trivy-security-scan.yml b/.gitea/workflows/trivy-security-scan.yml new file mode 100644 index 0000000..af12bb4 --- /dev/null +++ b/.gitea/workflows/trivy-security-scan.yml @@ -0,0 +1,161 @@ +name: Trivy Security Scan (Template) + +on: + workflow_call: + inputs: + images: + description: 'JSON array of images to build and scan. Each entry: {"name": "my-app", "dockerfile": "Dockerfile", "context": "."}' + required: false + type: string + default: '[]' + secrets: + GITHUB_TOKEN: + required: true + +jobs: + trivy-scan: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Trivy filesystem scan + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v $HOME/.cache:/root/.cache/ \ + -v ${{ github.workspace }}:/root/src \ + aquasec/trivy:canary fs --no-progress --severity HIGH,CRITICAL \ + --format json /root/src \ + > /tmp/fs-scan.json || true + + - name: Build and scan Docker images + run: | + IMAGES='${{ inputs.images }}' + echo "$IMAGES" | jq -c '.[]' | while read -r item; do + NAME=$(echo "$item" | jq -r '.name') + DOCKERFILE=$(echo "$item" | jq -r '.dockerfile') + CONTEXT=$(echo "$item" | jq -r '.context') + + echo "Building image: ${NAME}" + docker build -t "${NAME}:latest" -f "${DOCKERFILE}" "${CONTEXT}" + + echo "Scanning image: ${NAME}" + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v $HOME/.cache:/root/.cache/ \ + aquasec/trivy:canary image --no-progress --severity HIGH,CRITICAL \ + --format json "${NAME}:latest" \ + > "/tmp/image-scan-${NAME}.json" || true + done + + - name: Generate and publish security report to wiki + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + SCAN_DATE=$(date -u '+%Y-%m-%d %H:%M:%S UTC') + GITEA_URL="${{ github.server_url }}" + REPO="${{ github.repository }}" + COMMIT_SHA="${{ github.sha }}" + COMMIT_SHORT=$(echo "$COMMIT_SHA" | cut -c1-7) + + generate_report() { + local json_file="$1" + local title="$2" + + if ! jq empty "$json_file" 2>/dev/null; then + echo "## ${title}" + echo "" + echo "Failed to parse scan results." + echo "" + return + fi + + local total_vulns + total_vulns=$(jq '[.Results[]? | .Vulnerabilities[]?] | length' "$json_file") + local high_count + high_count=$(jq '[.Results[]? | .Vulnerabilities[]? | select(.Severity == "HIGH")] | length' "$json_file") + local critical_count + critical_count=$(jq '[.Results[]? | .Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length' "$json_file") + + echo "## ${title}" + echo "" + echo "| Total | CRITICAL | HIGH |" + echo "|-------|----------|------|" + echo "| ${total_vulns} | ${critical_count} | ${high_count} |" + echo "" + + if [ "$total_vulns" -eq 0 ]; then + echo "No vulnerabilities found." + echo "" + return + fi + + echo "| Package | Installed | Fixed | Vulnerability | Severity | Title |" + echo "|---------|-----------|-------|---------------|----------|-------|" + jq -r ' + [.Results[]? | .Vulnerabilities[]?] + | sort_by(if .Severity == "CRITICAL" then 0 else 1 end) + | .[] + | "| \(.PkgName) | \(.InstalledVersion) | \(.FixedVersion // "-") | \(.VulnerabilityID) | \(.Severity) | \(.Title // "-" | gsub("[\\n\\r]"; " ") | if length > 60 then .[:60] + "..." else . end) |" + ' "$json_file" + echo "" + } + + { + echo "# Security Scan Report" + echo "" + echo "| | |" + echo "|---|---|" + echo "| **Date** | ${SCAN_DATE} |" + echo "| **Commit** | ${COMMIT_SHORT} |" + echo "| **Branch** | ${{ github.ref_name }} |" + echo "| **Severity Filter** | HIGH, CRITICAL |" + echo "" + echo "---" + echo "" + generate_report /tmp/fs-scan.json "Filesystem Scan" + + IMAGES='${{ inputs.images }}' + echo "$IMAGES" | jq -c '.[]' | while read -r item; do + NAME=$(echo "$item" | jq -r '.name') + echo "---" + echo "" + generate_report "/tmp/image-scan-${NAME}.json" "Docker Image - ${NAME}" + done + } > /tmp/wiki-content.md + + base64 -w 0 /tmp/wiki-content.md > /tmp/content_b64.txt + + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + "${GITEA_URL}/api/v1/repos/${REPO}/wiki/page/security") + + if [ "$HTTP_CODE" = "200" ]; then + jq -n \ + --rawfile content /tmp/content_b64.txt \ + --arg message "Update security scan report - ${SCAN_DATE}" \ + --arg title "security" \ + '{content_base64: ($content | rtrimstr("\n")), message: $message, title: $title}' > /tmp/payload.json + + curl -s -X PATCH \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Content-Type: application/json" \ + -d @/tmp/payload.json \ + "${GITEA_URL}/api/v1/repos/${REPO}/wiki/page/security" + echo "Wiki page 'security' updated." + else + jq -n \ + --rawfile content /tmp/content_b64.txt \ + --arg message "Create security scan report - ${SCAN_DATE}" \ + --arg title "security" \ + '{content_base64: ($content | rtrimstr("\n")), message: $message, title: $title}' > /tmp/payload.json + + curl -s -X POST \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Content-Type: application/json" \ + -d @/tmp/payload.json \ + "${GITEA_URL}/api/v1/repos/${REPO}/wiki/new" + echo "Wiki page 'security' created." + fi