@@ -28,21 +28,42 @@ jobs:
2828 OUT_SARIF : " gitleaks.sarif"
2929 OUT_JSON : " gitleaks.json"
3030 OUT_LOG : " gitleaks.log"
31+ OUT_MD : " gitleaks-report.md"
3132 CONFIG_FILE : " .gitleaks.toml"
3233
3334 steps :
3435 - name : Checkout (PR-fast / Main-full)
3536 uses : actions/checkout@v4
3637 with :
37- # PR: shallow is fine (fast). Main push: full history for maximum coverage.
3838 fetch-depth : ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && 0 || 2 }}
3939
40- - name : Ensure output folder exists
40+ - name : Ensure output folder exists + baselines
4141 shell : bash
4242 run : |
4343 set -euo pipefail
4444 mkdir -p "${OUT_DIR}"
4545
46+ # Always create JSON baseline (empty array)
47+ echo "[]" > "${OUT_DIR}/${OUT_JSON}"
48+
49+ # Always create SARIF baseline
50+ cat > "${OUT_DIR}/${OUT_SARIF}" << 'EOF'
51+ {
52+ "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
53+ "version": "2.1.0",
54+ "runs": [
55+ { "tool": { "driver": { "name": "gitleaks", "version": "0" } }, "results": [] }
56+ ]
57+ }
58+ EOF
59+
60+ # Always create MD baseline
61+ cat > "${OUT_DIR}/${OUT_MD}" << 'EOF'
62+ # Gitleaks Secret Scan (Report-Only)
63+
64+ Status: Not executed yet.
65+ EOF
66+
4667 - name : Install Gitleaks (pinned)
4768 shell : bash
4869 run : |
@@ -57,18 +78,16 @@ jobs:
5778 if : always()
5879 shell : bash
5980 run : |
60- # Report-only: never fail the job
6181 set +e
6282
6383 CFG=()
6484 if [ -f "${CONFIG_FILE}" ]; then
6585 CFG=(--config="${CONFIG_FILE}")
6686 fi
6787
68- # PR: scan working tree only (no history) to keep it fast.
69- # Main push: full history (because fetch-depth=0).
7088 EXTRA=()
7189 if [ "${{ github.event_name }}" = "pull_request" ]; then
90+ # PR-fast: scan working tree only (no git history)
7291 EXTRA=(--no-git)
7392 fi
7493
@@ -81,14 +100,42 @@ jobs:
81100 --report-path="${OUT_DIR}/${OUT_JSON}" \
82101 2>&1 | tee "${OUT_DIR}/${OUT_LOG}"
83102
84- # Convert JSON -> SARIF (no re-scan)
103+ exit 0
104+
105+ - name : Build SARIF + Markdown report (from JSON/log)
106+ if : always()
107+ shell : bash
108+ run : |
109+ set -euo pipefail
85110 python3 - << 'PY'
86- import json, os, sys
87- out_dir = os.environ["OUT_DIR"]
111+ import json, os, re
112+
113+ out_dir = os.environ["OUT_DIR"]
88114 json_path = os.path.join(out_dir, os.environ["OUT_JSON"])
89- sarif_path = os.path.join(out_dir, os.environ["OUT_SARIF"])
90- ver = os.environ.get("GITLEAKS_VERSION","0")
115+ sarif_path= os.path.join(out_dir, os.environ["OUT_SARIF"])
116+ log_path = os.path.join(out_dir, os.environ["OUT_LOG"])
117+ md_path = os.path.join(out_dir, os.environ["OUT_MD"])
118+ ver = os.environ.get("GITLEAKS_VERSION","0")
91119
120+ # Read log (for error context)
121+ log_text = ""
122+ try:
123+ with open(log_path, "r", encoding="utf-8", errors="replace") as f:
124+ log_text = f.read()
125+ except Exception:
126+ pass
127+
128+ # Load findings JSON (fallback to empty list)
129+ findings = []
130+ try:
131+ with open(json_path, "r", encoding="utf-8") as f:
132+ data = json.load(f)
133+ if isinstance(data, list):
134+ findings = data
135+ except Exception:
136+ findings = []
137+
138+ # --- SARIF ---
92139 sarif = {
93140 "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
94141 "version": "2.1.0",
@@ -98,44 +145,68 @@ jobs:
98145 }]
99146 }
100147
101- try:
102- with open(json_path, "r", encoding="utf-8") as f:
103- data = json.load(f)
104- except Exception:
105- with open(sarif_path, "w", encoding="utf-8") as f:
106- json.dump(sarif, f, ensure_ascii=False, indent=2)
107- sys.exit(0)
108-
109- if isinstance(data, list):
110- for item in data:
111- file_path = item.get("File") or item.get("file") or item.get("Path") or ""
112- start_line = item.get("StartLine") or item.get("startLine") or item.get("Line") or item.get("line") or 1
113- rule_id = item.get("RuleID") or item.get("ruleID") or item.get("Rule") or "gitleaks"
114- desc = item.get("Description") or item.get("description") or "Potential secret detected"
115- msg = f"{desc} (rule: {rule_id})"
116-
117- try:
118- start_line = int(start_line)
119- except Exception:
120- start_line = 1
121-
122- sarif["runs"][0]["results"].append({
123- "ruleId": str(rule_id),
124- "level": "error",
125- "message": {"text": msg},
126- "locations": [{
127- "physicalLocation": {
128- "artifactLocation": {"uri": file_path},
129- "region": {"startLine": start_line}
130- }
131- }]
132- })
148+ def pick(d, *keys, default=None):
149+ for k in keys:
150+ if k in d and d[k] is not None:
151+ return d[k]
152+ return default
153+
154+ for item in findings:
155+ file_path = pick(item, "File", "file", "Path", "path", default="")
156+ start_line = pick(item, "StartLine", "startLine", "Line", "line", default=1)
157+ rule_id = pick(item, "RuleID", "ruleID", "Rule", "rule", default="gitleaks")
158+ desc = pick(item, "Description", "description", default="Potential secret detected")
159+
160+ try:
161+ start_line = int(start_line)
162+ except Exception:
163+ start_line = 1
164+
165+ sarif["runs"][0]["results"].append({
166+ "ruleId": str(rule_id),
167+ "level": "error",
168+ "message": {"text": f"{desc} (rule: {rule_id})"},
169+ "locations": [{
170+ "physicalLocation": {
171+ "artifactLocation": {"uri": file_path},
172+ "region": {"startLine": start_line}
173+ }
174+ }]
175+ })
133176
134177 with open(sarif_path, "w", encoding="utf-8") as f:
135178 json.dump(sarif, f, ensure_ascii=False, indent=2)
136- PY
137179
138- exit 0
180+ # --- Markdown report ---
181+ status = "OK"
182+ # Detect config failure or fatal error in log
183+ if re.search(r"\bFTL\b|\bFailed to load config\b", log_text, re.IGNORECASE):
184+ status = "ERROR (see log)"
185+
186+ md = []
187+ md.append("# Gitleaks Secret Scan (Report-Only)")
188+ md.append("")
189+ md.append(f"**Status:** {status}")
190+ md.append(f"**Mode:** {'PR-fast (working tree only)' if os.environ.get('GITHUB_EVENT_NAME','')=='pull_request' else 'Main (full history)'}")
191+ md.append(f"**Findings:** {len(findings)}")
192+ md.append("")
193+ if len(findings) == 0:
194+ md.append("No secrets detected (or scan produced no findings).")
195+ else:
196+ # Top rule counts
197+ counts = {}
198+ for x in findings:
199+ rid = pick(x, "RuleID", "ruleID", "Rule", "rule", default="gitleaks")
200+ counts[rid] = counts.get(rid, 0) + 1
201+ md.append("## Top Rules")
202+ for rid, c in sorted(counts.items(), key=lambda kv: kv[1], reverse=True)[:15]:
203+ md.append(f"- `{rid}`: {c}")
204+ md.append("")
205+ md.append("> Artifacts: `gitleaks.json`, `gitleaks.sarif`, `gitleaks.log`")
206+
207+ with open(md_path, "w", encoding="utf-8") as f:
208+ f.write("\n".join(md) + "\n")
209+ PY
139210
140211 - name : Upload artifacts (reports)
141212 if : always()
0 commit comments