Repository navigation
Test v0.3.0 summary schema #19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Security Scan | |
| on: | |
| push: | |
| branches: ["main"] | |
| pull_request: | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| security: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Prepare artifacts directory | |
| run: mkdir -p artifacts/security | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: "1.21" | |
| - name: Install dependencies | |
| run: go mod download | |
| - name: Set up Python for Semgrep | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Run Semgrep (CLI JSON) | |
| run: | | |
| pip install semgrep | |
| semgrep --config p/ci --json > artifacts/security/semgrep-report.json || true | |
| - name: Run Gitleaks | |
| uses: gitleaks/gitleaks-action@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Run Trivy FS Scan | |
| uses: aquasecurity/trivy-action@master | |
| with: | |
| scan-type: fs | |
| format: json | |
| output: artifacts/security/trivy-fs.json | |
| severity: HIGH,CRITICAL | |
| - name: Generate security summary (JSON) | |
| if: always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const dir = 'artifacts/security'; | |
| const summaryPath = path.join(dir, 'summary.json'); | |
| if (!fs.existsSync(dir)) { | |
| fs.mkdirSync(dir, { recursive: true }); | |
| } | |
| function readJson(file) { | |
| if (!fs.existsSync(file)) return null; | |
| const raw = fs.readFileSync(file, 'utf8'); | |
| try { return JSON.parse(raw); } | |
| catch { return null; } | |
| } | |
| const result = { | |
| version: "0.3.0", | |
| status: "UNKNOWN", | |
| blocking_count: 0, | |
| summary: {}, | |
| findings: [] | |
| }; | |
| // | |
| // GITLEAKS | |
| // | |
| const gitleaksPath = path.join(dir, 'gitleaks-report.json'); | |
| const gitleaks = readJson(gitleaksPath); | |
| if (gitleaks) { | |
| let count = 0; | |
| if (Array.isArray(gitleaks)) count = gitleaks.length; | |
| else if (Array.isArray(gitleaks.findings)) count = gitleaks.findings.length; | |
| result.summary.gitleaks = { total: count }; | |
| } | |
| // | |
| // TRIVY FS | |
| // | |
| const trivyFsPath = path.join(dir, 'trivy-fs.json'); | |
| const trivy = readJson(trivyFsPath); | |
| if (trivy?.Results) { | |
| const counts = { critical: 0, high: 0, medium: 0, low: 0 }; | |
| for (const r of trivy.Results) { | |
| for (const v of (r.Vulnerabilities || [])) { | |
| const sev = (v.Severity || "").toLowerCase(); | |
| if (counts[sev] !== undefined) counts[sev]++; | |
| } | |
| } | |
| result.summary.trivy_fs = counts; | |
| } | |
| // | |
| // SEMGREP | |
| // | |
| const semgrepPath = path.join(dir, 'semgrep-report.json'); | |
| const semgrep = readJson(semgrepPath); | |
| if (semgrep) { | |
| let results = []; | |
| // formatos posibles: | |
| // - { "results": [...] } | |
| // - o un array directo | |
| if (Array.isArray(semgrep)) { | |
| results = semgrep; | |
| } else if (Array.isArray(semgrep.results)) { | |
| results = semgrep.results; | |
| } | |
| result.summary.semgrep = { total: results.length }; | |
| core.info(`Semgrep findings: ${results.length}`); | |
| } else { | |
| core.info("No semgrep-report.json found, skipping Semgrep summary."); | |
| } | |
| fs.writeFileSync(summaryPath, JSON.stringify(result, null, 2)); | |
| core.info("Wrote summary.json"); | |
| - name: Post PR Security Summary | |
| if: always() && github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const fs = require('fs'); | |
| const marker = '<!-- devsecops-kit-security-summary -->'; | |
| const summaryPath = 'artifacts/security/summary.json'; | |
| let summary = null; | |
| try { summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8')); } | |
| catch { core.warning("No summary.json available."); } | |
| let body = `${marker}\n### 🔐 DevSecOps Kit Security Summary\n\n`; | |
| if (!summary) { | |
| body += "_No summary available._\n"; | |
| } else { | |
| const leaks = summary.summary?.gitleaks?.total ?? 0; | |
| const trivy = summary.summary?.trivy_fs ?? {}; | |
| const semgrep = summary.summary?.semgrep ?? null; | |
| body += `- **Gitleaks:** ${leaks} leak(s)\n`; | |
| if (Object.keys(trivy).length > 0) { | |
| body += `- **Trivy FS:**\n`; | |
| for (const sev of Object.keys(trivy)) { | |
| body += ` - ${sev.toUpperCase()}: ${trivy[sev]}\n`; | |
| } | |
| } | |
| if (semgrep) { | |
| body += `- **Semgrep:** ${semgrep.total} finding(s)\n`; | |
| } | |
| const blocking = | |
| leaks > 0 || | |
| (trivy.critical ?? 0) > 0 || | |
| (trivy.high ?? 0) > 0; | |
| body += `\n${blocking ? '🚨 _Blocking issues detected._' : '✅ _No blocking issues detected._'}\n`; | |
| } | |
| const { owner, repo } = context.repo; | |
| const pr = context.issue.number; | |
| const comments = await github.rest.issues.listComments({ owner, repo, issue_number: pr }); | |
| const existing = comments.data.find(c => c.body?.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner, repo, | |
| comment_id: existing.id, | |
| body | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner, repo, | |
| issue_number: pr, | |
| body | |
| }); | |
| } | |
| - name: Upload security artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: security-reports | |
| path: artifacts/security/ |