Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/compute-entropy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -78,4 +78,4 @@ def shannon_entropy(text: str) -> float:
}, f, indent=2)

print(f"Average entropy: {avg}")
print(verdict)
print(verdict)
68 changes: 68 additions & 0 deletions .github/workflows/entropy-beauty-comment.yml
Original file line number Diff line number Diff line change
@@ -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
});
188 changes: 95 additions & 93 deletions .github/workflows/entropy-beauty-scan.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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:
Expand All @@ -53,99 +58,96 @@ 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`;

if (beauty.files && beauty.files.length) {
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");
Loading