diff --git a/.github/workflows/compute-entropy.py b/.github/workflows/compute-entropy.py index 44a11770a7..9abe8bb9a1 100644 --- a/.github/workflows/compute-entropy.py +++ b/.github/workflows/compute-entropy.py @@ -21,7 +21,7 @@ def shannon_entropy(text: str) -> float: head_sha = os.environ.get("HEAD_SHA", "").strip() or "HEAD" try: - if event_name == "pull_request_target" and base_sha: + if event_name in ("pull_request", "pull_request_target") and base_sha: # PR case: we checked out the PR head and fetched the base commit changed_files = subprocess.check_output( ["git", "diff", "--name-only", base_sha, head_sha], @@ -78,4 +78,4 @@ def shannon_entropy(text: str) -> float: }, f, indent=2) print(f"Average entropy: {avg}") -print(verdict) \ No newline at end of file +print(verdict) diff --git a/.github/workflows/entropy-beauty-comment.yml b/.github/workflows/entropy-beauty-comment.yml new file mode 100644 index 0000000000..e0d7e9c904 --- /dev/null +++ b/.github/workflows/entropy-beauty-comment.yml @@ -0,0 +1,68 @@ +name: Post Entropy Beauty Comment + +on: + workflow_run: + workflows: ["Entropy Beauty + TruffleHog Scan"] + types: [completed] + +permissions: + contents: read + pull-requests: write + +jobs: + comment: + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + steps: + - name: Download scan results + uses: actions/download-artifact@v4 + with: + name: scan-results + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Post summary comment + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + if (!fs.existsSync('scan-summary.json')) { + core.setFailed('scan-summary.json missing — analysis did not produce results'); + return; + } + + const summary = JSON.parse(fs.readFileSync('scan-summary.json', 'utf8')); + const beauty = summary.beauty || {}; + const findingsCount = summary.findings_count || 0; + + let body = `## 🐷 TruffleHog + Entropy Beauty Scan\n\n`; + body += `**Average entropy of changed code:** ${beauty.average_entropy} bits/char\n`; + body += `**Verdict:** ${beauty.verdict}\n\n`; + + if (beauty.files && beauty.files.length) { + body += `**Changed files entropy:**\n\`\`\`\n${beauty.files.join('\n')}\n\`\`\`\n\n`; + } + + if (findingsCount > 0) { + body += `⚠️ **TruffleHog found ${findingsCount} potential issue(s)**\n`; + } else { + body += `✅ No secrets or suspicious high-entropy strings found.\n`; + } + + body += `\n*Mid-4 beauty heuristic in action — powered by our entropy chats! 😊*`; + + const prs = github.event.workflow_run.pull_requests || []; + if (prs.length === 0) { + console.log('No associated PR found'); + return; + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prs[0].number, + body: body + }); diff --git a/.github/workflows/entropy-beauty-scan.yml b/.github/workflows/entropy-beauty-scan.yml index 0c9056bdca..16843d599c 100644 --- a/.github/workflows/entropy-beauty-scan.yml +++ b/.github/workflows/entropy-beauty-scan.yml @@ -1,11 +1,12 @@ name: Entropy Beauty + TruffleHog Scan -on: [push, release, pull_request_target] +on: + push: + release: + pull_request: permissions: contents: read - pull-requests: write - issues: write # must be at workflow level for push/merge events jobs: scan: @@ -14,37 +15,41 @@ jobs: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} - fetch-depth: ${{ github.event_name == 'pull_request_target' && 1 || 2 }} - allow-unsafe-pr-checkout: ${{ github.event_name == 'pull_request_target' }} - - - name: Fetch PR base commit (needed for accurate diff) - if: github.event_name == 'pull_request_target' - run: git fetch origin ${{ github.event.pull_request.base.sha }} --depth=1 - - - name: Cache pip manually - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-entropy-${{ hashFiles('.github/workflows/compute-entropy.py') }} - restore-keys: | - ${{ runner.os }}-pip-entropy- + fetch-depth: 0 + persist-credentials: false - name: Setup Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.12' - - name: Install Python dependencies (only when needed) - run: | - python -m pip install --upgrade pip - # No extra packages needed — compute-entropy.py uses only stdlib + - name: Cache TruffleHog Docker image + id: cache-trufflehog + uses: actions/cache@v4 + with: + path: /tmp/trufflehog-image.tar + key: trufflehog-3.96.0-${{ runner.os }} + - name: Load TruffleHog image from cache + if: steps.cache-trufflehog.outputs.cache-hit == 'true' + run: docker load -i /tmp/trufflehog-image.tar + + - name: Pull TruffleHog image (cache miss) + if: steps.cache-trufflehog.outputs.cache-hit != 'true' + run: | + docker pull trufflesecurity/trufflehog:3.96.0 + docker save trufflesecurity/trufflehog:3.96.0 -o /tmp/trufflehog-image.tar - name: Run TruffleHog - uses: trufflesecurity/trufflehog@6f3c981e7b77f235fd2702dd74af25fc4b72bf11 # v3.96.0 - with: - path: . - extra_args: --results=verified,unknown --filter-entropy=3.5 --json + run: | + docker run --rm \ + -v "$PWD:/pwd" \ + trufflesecurity/trufflehog:3.96.0 \ + git file:///pwd \ + --results=verified,unknown \ + --filter-entropy=3.5 \ + --json \ + --no-update \ + > trufflehog.json 2>/dev/null || true - name: Compute mid-4 beauty entropy env: @@ -53,78 +58,77 @@ jobs: HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} run: python .github/workflows/compute-entropy.py - - name: Post summary comment (PR only) - if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target' - uses: actions/github-script@v9 + - name: Create sanitized summary (never include Raw secrets) + run: | + python3 << 'EOF' + import json, os + from pathlib import Path + + findings = [] + if Path("trufflehog.json").exists(): + for line in Path("trufflehog.json").read_text().splitlines(): + line = line.strip() + if not line: + continue + try: + f = json.loads(line) + findings.append({ + "detector": f.get("DetectorName"), + "verified": f.get("Verified"), + "file": f.get("SourceMetadata", {}).get("Data", {}).get("Git", {}).get("file"), + "line": f.get("SourceMetadata", {}).get("Data", {}).get("Git", {}).get("line"), + }) + except Exception: + pass + + beauty = {} + if Path("/tmp/beauty.json").exists(): + beauty = json.loads(Path("/tmp/beauty.json").read_text()) + + summary = { + "beauty": beauty, + "findings_count": len(findings), + "findings": findings, # safe fields only + } + Path("scan-summary.json").write_text(json.dumps(summary, indent=2)) + print(f"Sanitized summary written ({len(findings)} findings)") + EOF + + - name: Upload sanitized results only + uses: actions/upload-artifact@v4 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - - // Read TruffleHog output — it prints one JSON object per line (NDJSON) - let findings = []; - if (fs.existsSync('trufflehog.json')) { - try { - const lines = fs.readFileSync('trufflehog.json', 'utf8').trim().split('\n'); - findings = lines.map(line => { - try { return JSON.parse(line); } catch(e) { return null; } - }).filter(Boolean); - } catch(e) {} - } else { - console.log("No trufflehog.json found, using empty findings"); - } - - const beauty = JSON.parse(fs.readFileSync('/tmp/beauty.json', 'utf8')); - - let body = `## 🐷 TruffleHog + Entropy Beauty Scan\n\n`; - body += `**Average entropy of changed code:** ${beauty.average_entropy} bits/char\n`; - body += `**Verdict:** ${beauty.verdict}\n\n`; - - if (beauty.files && beauty.files.length) { - body += `**Changed files entropy:**\n\`\`\`\n${beauty.files.join('\n')}\n\`\`\`\n\n`; - } - - if (findings.length > 0) { - body += `⚠️ **TruffleHog found ${findings.length} potential issue(s)**\n`; - } else { - body += `✅ No secrets or suspicious high-entropy strings found.\n`; - } - - body += `\n*Mid-4 beauty heuristic in action — powered by our entropy chats! 😊*`; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: body - }); + name: scan-results + path: scan-summary.json + retention-days: 1 + + # Separate job that only runs on trusted events and has write permission + create-issue: + needs: scan + if: github.event_name == 'push' || github.event_name == 'release' + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - name: Download summary + uses: actions/download-artifact@v4 + with: + name: scan-results - # ── Create issue on push ONLY if suspicious (entropy outside 4.3–5.1) ── - - name: Create issue on suspicious push - if: github.event_name == 'push' || github.event_name == 'release' - uses: actions/github-script@v9 + - name: Create issue on suspicious entropy + uses: actions/github-script@v7 with: - github-token: ${{ secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); - const beauty = JSON.parse(fs.readFileSync('/tmp/beauty.json', 'utf8')); + const summary = JSON.parse(fs.readFileSync('scan-summary.json', 'utf8')); + const beauty = summary.beauty || {}; + const findingsCount = summary.findings_count || 0; - // Only create issue if it's NOT beautiful mid-4 if (beauty.average_entropy >= 4.3 && beauty.average_entropy <= 5.1) { console.log("✅ Mid-4 beauty — no issue created"); return; } - let findings = []; - if (fs.existsSync('trufflehog.json')) { - try { - const lines = fs.readFileSync('trufflehog.json', 'utf8').trim().split('\n'); - findings = lines.map(line => { - try { return JSON.parse(line); } catch(e) { return null; } - }).filter(Boolean); - } catch(e) {} - } - let body = `**Average entropy:** ${beauty.average_entropy} bits/char\n\n`; body += `**Verdict:** ${beauty.verdict}\n\n`; @@ -132,20 +136,18 @@ jobs: body += `**Changed files:**\n\`\`\`\n${beauty.files.join('\n')}\n\`\`\`\n\n`; } - if (findings.length > 0) { - body += `**TruffleHog found ${findings.length} potential issue(s)**\n`; + if (findingsCount > 0) { + body += `**TruffleHog found ${findingsCount} potential issue(s)**\n`; } else { body += `✅ No secrets or suspicious high-entropy strings found.\n`; } - body += `\n*Triggered by push to \`${context.sha}\` — mid-4 beauty heuristic*`; + body += `\n*Triggered by ${context.eventName} to \`${context.sha}\` — mid-4 beauty heuristic*`; await github.rest.issues.create({ owner: context.repo.owner, repo: context.repo.repo, - title: `🚨 Suspicious entropy detected in recent push (${beauty.average_entropy})`, + title: `🚨 Suspicious entropy detected in recent ${context.eventName} (${beauty.average_entropy})`, body: body, labels: ['entropy', 'security', 'review-needed'] }); - - console.log("⚠️ Created issue because entropy was outside mid-4 range");