Refine PowerShell script descriptions in README #158
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
| # .github/workflows/secret-scan-gitleaks.yml | |
| name: Secret Scan (Gitleaks) [Report-Only] | |
| on: | |
| pull_request: | |
| branches: [main, develop] | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| concurrency: | |
| group: secret-scan-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| security-events: write | |
| jobs: | |
| gitleaks: | |
| name: Gitleaks Scan (PR-fast / Main-full) [Report-Only] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| env: | |
| GITLEAKS_VERSION: "8.30.0" | |
| OUT_DIR: "secret-scan-reports" | |
| OUT_SARIF: "gitleaks.sarif" | |
| OUT_JSON: "gitleaks.json" | |
| OUT_LOG: "gitleaks.log" | |
| OUT_MD: "gitleaks-report.md" | |
| CONFIG_FILE: ".gitleaks.toml" | |
| steps: | |
| - name: Checkout (PR-fast / Main-full) | |
| uses: actions/checkout@v4 | |
| with: | |
| # PR: shallow checkout for speed. Push to main: full history for maximum coverage. | |
| fetch-depth: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && 0 || 2 }} | |
| - name: Ensure output folder exists + baselines | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "${OUT_DIR}" | |
| # Always create JSON baseline (empty array) | |
| echo "[]" > "${OUT_DIR}/${OUT_JSON}" | |
| # Always create SARIF baseline | |
| cat > "${OUT_DIR}/${OUT_SARIF}" << 'EOF' | |
| { | |
| "$schema": "https://json.schemastore.org/sarif-2.1.0.json", | |
| "version": "2.1.0", | |
| "runs": [ | |
| { "tool": { "driver": { "name": "gitleaks", "version": "0" } }, "results": [] } | |
| ] | |
| } | |
| EOF | |
| # Always create MD baseline | |
| cat > "${OUT_DIR}/${OUT_MD}" << 'EOF' | |
| # Gitleaks Secret Scan (Report-Only) | |
| Status: Not executed yet. | |
| EOF | |
| - name: Install Gitleaks (pinned) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| VER="${GITLEAKS_VERSION}" | |
| curl -sSL -o /tmp/gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${VER}/gitleaks_${VER}_linux_x64.tar.gz" | |
| tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks | |
| sudo mv /tmp/gitleaks /usr/local/bin/gitleaks | |
| gitleaks version | |
| - name: Run Gitleaks (PR-fast / Main-full) [Report-Only] | |
| if: always() | |
| shell: bash | |
| run: | | |
| # Report-only: never fail the job | |
| set +e | |
| CFG=() | |
| if [ -f "${CONFIG_FILE}" ]; then | |
| CFG=(--config="${CONFIG_FILE}") | |
| fi | |
| EXTRA=() | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| # PR-fast: scan working tree only (no git history) | |
| EXTRA=(--no-git) | |
| fi | |
| gitleaks detect \ | |
| --source="." \ | |
| --redact \ | |
| "${CFG[@]}" \ | |
| "${EXTRA[@]}" \ | |
| --report-format="json" \ | |
| --report-path="${OUT_DIR}/${OUT_JSON}" \ | |
| 2>&1 | tee "${OUT_DIR}/${OUT_LOG}" | |
| exit 0 | |
| - name: Build SARIF + Markdown report (from JSON/log) | |
| if: always() | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python3 - << 'PY' | |
| import json, os, re | |
| out_dir = os.environ["OUT_DIR"] | |
| json_path = os.path.join(out_dir, os.environ["OUT_JSON"]) | |
| sarif_path = os.path.join(out_dir, os.environ["OUT_SARIF"]) | |
| log_path = os.path.join(out_dir, os.environ["OUT_LOG"]) | |
| md_path = os.path.join(out_dir, os.environ["OUT_MD"]) | |
| ver = os.environ.get("GITLEAKS_VERSION","0") | |
| # Read log (for error context) | |
| log_text = "" | |
| try: | |
| with open(log_path, "r", encoding="utf-8", errors="replace") as f: | |
| log_text = f.read() | |
| except Exception: | |
| pass | |
| # Load findings JSON (fallback to empty list) | |
| findings = [] | |
| try: | |
| with open(json_path, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| if isinstance(data, list): | |
| findings = data | |
| except Exception: | |
| findings = [] | |
| # --- SARIF --- | |
| sarif = { | |
| "$schema": "https://json.schemastore.org/sarif-2.1.0.json", | |
| "version": "2.1.0", | |
| "runs": [{ | |
| "tool": {"driver": {"name": "gitleaks", "version": ver}}, | |
| "results": [] | |
| }] | |
| } | |
| def pick(d, *keys, default=None): | |
| for k in keys: | |
| if k in d and d[k] is not None: | |
| return d[k] | |
| return default | |
| for item in findings: | |
| file_path = pick(item, "File", "file", "Path", "path", default="") | |
| start_line = pick(item, "StartLine", "startLine", "Line", "line", default=1) | |
| rule_id = pick(item, "RuleID", "ruleID", "Rule", "rule", default="gitleaks") | |
| desc = pick(item, "Description", "description", default="Potential secret detected") | |
| try: | |
| start_line = int(start_line) | |
| except Exception: | |
| start_line = 1 | |
| sarif["runs"][0]["results"].append({ | |
| "ruleId": str(rule_id), | |
| "level": "error", | |
| "message": {"text": f"{desc} (rule: {rule_id})"}, | |
| "locations": [{ | |
| "physicalLocation": { | |
| "artifactLocation": {"uri": file_path}, | |
| "region": {"startLine": start_line} | |
| } | |
| }] | |
| }) | |
| with open(sarif_path, "w", encoding="utf-8") as f: | |
| json.dump(sarif, f, ensure_ascii=False, indent=2) | |
| # --- Markdown report --- | |
| status = "OK" | |
| if re.search(r"\bFTL\b|\bFailed to load config\b", log_text, re.IGNORECASE): | |
| status = "ERROR (see log)" | |
| mode = "Main (full history)" | |
| if os.environ.get("GITHUB_EVENT_NAME","") == "pull_request": | |
| mode = "PR-fast (working tree only)" | |
| md = [] | |
| md.append("# Gitleaks Secret Scan (Report-Only)") | |
| md.append("") | |
| md.append(f"**Status:** {status}") | |
| md.append(f"**Mode:** {mode}") | |
| md.append(f"**Findings:** {len(findings)}") | |
| md.append("") | |
| if len(findings) == 0: | |
| md.append("No secrets detected (or scan produced no findings).") | |
| else: | |
| counts = {} | |
| for x in findings: | |
| rid = pick(x, "RuleID", "ruleID", "Rule", "rule", default="gitleaks") | |
| counts[rid] = counts.get(rid, 0) + 1 | |
| md.append("## Top Rules") | |
| for rid, c in sorted(counts.items(), key=lambda kv: kv[1], reverse=True)[:15]: | |
| md.append(f"- `{rid}`: {c}") | |
| md.append("") | |
| md.append("> Artifacts: `gitleaks.json`, `gitleaks.sarif`, `gitleaks.log`, `gitleaks-report.md`") | |
| with open(md_path, "w", encoding="utf-8") as f: | |
| f.write("\n".join(md) + "\n") | |
| PY | |
| - name: Publish report to Run Summary (View Runs) | |
| if: always() | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| COUNT="$(python3 -c 'import json; import sys; print(len(json.load(open(sys.argv[1]))))' "${OUT_DIR}/${OUT_JSON}" 2>/dev/null || echo 0)" | |
| { | |
| echo "## 🔐 Gitleaks Secret Scan (Report-Only)" | |
| echo "" | |
| echo "**Workflow:** PR-fast / Main-full" | |
| echo "**Event:** \`${{ github.event_name }}\`" | |
| echo "**Ref:** \`${{ github.ref }}\`" | |
| echo "**Findings:** ${COUNT}" | |
| echo "" | |
| echo "### Report" | |
| echo "" | |
| if [ -f "${OUT_DIR}/${OUT_MD}" ]; then | |
| cat "${OUT_DIR}/${OUT_MD}" | |
| else | |
| echo "_No markdown report file found:_ \`${OUT_DIR}/${OUT_MD}\`" | |
| fi | |
| echo "" | |
| echo "### Artifacts" | |
| echo "" | |
| echo "- \`${OUT_DIR}/${OUT_JSON}\`" | |
| echo "- \`${OUT_DIR}/${OUT_SARIF}\`" | |
| echo "- \`${OUT_DIR}/${OUT_LOG}\`" | |
| echo "- \`${OUT_DIR}/${OUT_MD}\`" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload artifacts (reports) | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: secret-scan-reports | |
| path: ${{ env.OUT_DIR }}/** | |
| if-no-files-found: warn | |
| retention-days: 30 | |
| - name: Upload SARIF to GitHub Code Scanning | |
| if: always() && hashFiles(format('{0}/{1}', env.OUT_DIR, env.OUT_SARIF)) != '' | |
| uses: github/codeql-action/upload-sarif@v4 | |
| with: | |
| sarif_file: ${{ env.OUT_DIR }}/${{ env.OUT_SARIF }} | |
| category: secrets/gitleaks |