1+ # .github/workflows/secret-scan-gitleaks.yml
12name : Secret Scan (Gitleaks) [Report-Only]
23
34on :
@@ -23,11 +24,11 @@ jobs:
2324
2425 env :
2526 GITLEAKS_VERSION : " 8.30.0"
26- OUT_DIR : secret-scan-reports
27- OUT_SARIF : gitleaks.sarif
28- OUT_JSON : gitleaks.json
29- OUT_LOG : gitleaks.log
30- CONFIG_FILE : .gitleaks.toml
27+ OUT_DIR : " secret-scan-reports"
28+ OUT_SARIF : " gitleaks.sarif"
29+ OUT_JSON : " gitleaks.json"
30+ OUT_LOG : " gitleaks.log"
31+ CONFIG_FILE : " .gitleaks.toml"
3132
3233 steps :
3334 - name : Checkout (full history)
@@ -36,52 +37,95 @@ jobs:
3637 fetch-depth : 0
3738
3839 - name : Ensure output folder exists
39- run : mkdir -p "${{ env.OUT_DIR }}"
40-
41- - name : Initialize empty SARIF baseline
42- if : always()
40+ shell : bash
4341 run : |
44- cat > "${{ env.OUT_DIR }}/${{ env.OUT_SARIF }}" << 'EOF'
45- {
46- "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
47- "version": "2.1.0",
48- "runs": [
49- { "tool": { "driver": { "name": "gitleaks", "version": "0" } }, "results": [] }
50- ]
51- }
52- EOF
42+ set -euo pipefail
43+ mkdir -p "${OUT_DIR}"
5344
5445 - name : Install Gitleaks (pinned)
55- if : always()
46+ shell : bash
5647 run : |
5748 set -euo pipefail
58- VER="${{ env. GITLEAKS_VERSION } }"
49+ VER="${GITLEAKS_VERSION}"
5950 curl -sSL -o /tmp/gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${VER}/gitleaks_${VER}_linux_x64.tar.gz"
6051 tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks
6152 sudo mv /tmp/gitleaks /usr/local/bin/gitleaks
6253 gitleaks version
6354
64- - name : Run Gitleaks (SARIF + JSON + Log ) [Report-Only]
55+ - name : Run Gitleaks (single run ) [Report-Only]
6556 if : always()
57+ shell : bash
6658 run : |
59+ # Report-only: never fail the job
6760 set +e
6861
69- CFG=""
70- if [ -f "${{ env. CONFIG_FILE } }" ]; then
71- CFG=" --config=${{ env. CONFIG_FILE }}"
62+ CFG=()
63+ if [ -f "${CONFIG_FILE}" ]; then
64+ CFG=( --config="${ CONFIG_FILE}")
7265 fi
7366
74- # SARIF
75- gitleaks detect --source="." --redact $CFG \
76- --report-format=sarif \
77- --report-path="${{ env.OUT_DIR }}/${{ env.OUT_SARIF }}" \
78- 2>&1 | tee "${{ env.OUT_DIR }}/${{ env.OUT_LOG }}"
79-
80- # JSON (second report output; keep report-only)
81- gitleaks detect --source="." --redact $CFG \
82- --report-format=json \
83- --report-path="${{ env.OUT_DIR }}/${{ env.OUT_JSON }}" \
84- >/dev/null 2>&1
67+ # Run once, produce JSON, and capture log. Exit code is ignored intentionally.
68+ gitleaks detect \
69+ --source="." \
70+ --redact \
71+ "${CFG[@]}" \
72+ --report-format="json" \
73+ --report-path="${OUT_DIR}/${OUT_JSON}" \
74+ 2>&1 | tee "${OUT_DIR}/${OUT_LOG}"
75+
76+ # Create SARIF from JSON (single scan, dual outputs)
77+ python3 - << 'PY'
78+ import json, os, sys
79+ out_dir = os.environ["OUT_DIR"]
80+ json_path = os.path.join(out_dir, os.environ["OUT_JSON"])
81+ sarif_path = os.path.join(out_dir, os.environ["OUT_SARIF"])
82+
83+ # Baseline SARIF skeleton
84+ sarif = {
85+ "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
86+ "version": "2.1.0",
87+ "runs": [{
88+ "tool": {"driver": {"name": "gitleaks", "version": os.environ.get("GITLEAKS_VERSION","0")}},
89+ "results": []
90+ }]
91+ }
92+
93+ try:
94+ with open(json_path, "r", encoding="utf-8") as f:
95+ data = json.load(f)
96+ except Exception:
97+ # If JSON missing or invalid, still write baseline SARIF
98+ with open(sarif_path, "w", encoding="utf-8") as f:
99+ json.dump(sarif, f, ensure_ascii=False, indent=2)
100+ sys.exit(0)
101+
102+ # Gitleaks JSON is typically a list of findings
103+ if isinstance(data, list):
104+ for item in data:
105+ file_path = item.get("File") or item.get("file") or item.get("Path") or ""
106+ start_line = item.get("StartLine") or item.get("startLine") or item.get("Line") or item.get("line") or 1
107+ rule_id = item.get("RuleID") or item.get("ruleID") or item.get("Rule") or "gitleaks"
108+ desc = item.get("Description") or item.get("description") or "Potential secret detected"
109+ secret = item.get("Secret") or item.get("secret") or ""
110+ # Keep message minimal (avoid reproducing secrets)
111+ msg = f"{desc} (rule: {rule_id})"
112+
113+ res = {
114+ "ruleId": str(rule_id),
115+ "level": "error",
116+ "message": {"text": msg},
117+ "locations": [{
118+ "physicalLocation": {
119+ "artifactLocation": {"uri": file_path},
120+ "region": {"startLine": int(start_line) if str(start_line).isdigit() else 1}
121+ }
122+ }]
123+ }
124+ sarif["runs"][0]["results"].append(res)
125+
126+ with open(sarif_path, "w", encoding="utf-8") as f:
127+ json.dump(sarif, f, ensure_ascii=False, indent=2)
128+ PY
85129
86130 exit 0
87131
0 commit comments