1+ ---
2+ # SPDX-FileCopyrightText: (C) 2025 Intel Corporation
3+ # SPDX-License-Identifier: Apache-2.0
4+ # Gitleaks Secret Scanner Action
5+ #
6+ # This composite action performs secret scanning using Gitleaks,
7+ # supporting full repository scans or changed-file-only mode, with SARIF reporting.
8+ #
9+ # Key Features:
10+ # - Detects hardcoded secrets and credentials
11+ # - Supports custom configuration and baselines
12+ # - Redaction of sensitive values in logs
13+ # - SARIF output for GitHub Code Scanning integration
14+ #
15+ # Process Stages:
16+ # 1. Environment Setup:
17+ # - Gitleaks installation (specific or latest version)
18+ #
19+ # 2. Scan Execution:
20+ # - Full repository or changed files only
21+ # - Apply custom config and baseline if provided
22+ #
23+ # 3. Results Processing:
24+ # - Report generation in SARIF/JSON/CSV
25+ # - Artifact upload
26+ # - Optional SARIF upload to GitHub Security tab
27+ #
28+ # Required Inputs:
29+ # - scan-scope: Scope of scan (all or changed)
30+ # - source: Path to scan
31+ #
32+ # Optional Inputs:
33+ # - config_path: Path to custom Gitleaks config
34+ # - baseline_path: Path to baseline file
35+ # - report_format: sarif, json, or csv (default: sarif)
36+ # - redact: Redact secrets in output (true/false)
37+ #
38+ # Outputs:
39+ # - exit_code: Gitleaks exit code
40+ # - report_path: Path to generated report
41+ #
42+ # Example Usage:
43+ # steps:
44+ # - uses: ./.github/actions/security/gitleaks
45+ # with:
46+ # scan-scope: "changed"
47+ # source: "./src"
48+ # config_path: "./ci/gitleaks_baselines/<repo-name>-gitleaks.json"
49+ # report_format: "json"
50+ #
51+ # Note: Requires `security-events: write` permission for SARIF upload.
52+
53+ name : " Gitleaks Secret Scanner"
54+ description : " Detect leaked secrets with Gitleaks. Supports full repo or only-changed files, SARIF upload, baselines, and custom config."
55+
56+ inputs :
57+ scan-scope :
58+ description : " Scope of files to scan (all/changed)"
59+ required : false
60+ default : " changed"
61+ source :
62+ description : " Path to scan (repository root by default)"
63+ required : false
64+ default : " ."
65+ version :
66+ description : " Gitleaks version: 'latest' or a specific version (e.g., 8.28.0)"
67+ required : false
68+ default : " latest"
69+ config_path :
70+ description : " Path to a .gitleaks.toml config (if omitted, Gitleaks uses its defaults)"
71+ required : false
72+ default : " "
73+ baseline_path :
74+ description : " Path to a baseline file to ignore previously known leaks"
75+ required : false
76+ default : " "
77+ report_format :
78+ description : " Output format (sarif,json,csv)"
79+ required : false
80+ default : " sarif"
81+ redact :
82+ description : " Redact secrets in logs/report (true/false)"
83+ required : false
84+ default : " true"
85+ exit_code_on_leak :
86+ description : " Exit code to use when leaks are found (0 to never fail, 1 default)"
87+ required : false
88+ default : " 1"
89+ report_suffix :
90+ description : " Suffix for artifact/report names (e.g., -linux, -windows). Include the leading '-'"
91+ required : false
92+ default : " "
93+
94+ outputs :
95+ exit_code :
96+ description : " Exit code from gitleaks detect"
97+ value : ${{ steps.run-gitleaks.outputs.exit_code }}
98+ report_path :
99+ description : " Path to the generated report file"
100+ value : ${{ steps.run-gitleaks.outputs.report_path }}
101+
102+ runs :
103+ using : " composite"
104+ steps :
105+ - name : Get changed files
106+ if : inputs['scan-scope'] == 'changed'
107+ id : changed-files
108+ uses : tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
109+
110+ - name : Install Gitleaks
111+ id : install
112+ shell : bash
113+ env :
114+ INPUT_VERSION : ${{ inputs.version }}
115+ run : |
116+ set -euo pipefail
117+ VER="${INPUT_VERSION}"
118+
119+ if [[ "$VER" == "latest" ]]; then
120+ # Resolve latest tag (e.g., v8.28.0) and strip the 'v' for the tarball name
121+ VER=$(curl -s https://api.github.com/repos/gitleaks/gitleaks/releases/latest \
122+ | grep -Po '"tag_name":\s*"v\K[0-9.]+' || true)
123+ if [[ -z "$VER" ]]; then
124+ echo "::error::Failed to resolve latest Gitleaks version"
125+ exit 1
126+ fi
127+ fi
128+
129+ echo "Installing Gitleaks version: $VER"
130+ curl -sSL \
131+ -o /tmp/gitleaks.tar.gz \
132+ "https://github.com/gitleaks/gitleaks/releases/download/v${VER}/gitleaks_${VER}_linux_x64.tar.gz"
133+ sudo tar xf /tmp/gitleaks.tar.gz -C /usr/local/bin gitleaks
134+ rm -f /tmp/gitleaks.tar.gz
135+ gitleaks version || (echo "::error::Gitleaks failed to install" && exit 1)
136+
137+ - name : Run Gitleaks
138+ id : run-gitleaks
139+ shell : bash
140+ env :
141+ INPUT_SCOPE : ${{ inputs['scan-scope'] }}
142+ INPUT_SOURCE : ${{ inputs.source }}
143+ INPUT_CONFIG : ${{ inputs.config_path }}
144+ INPUT_BASELINE : ${{ inputs.baseline_path }}
145+ INPUT_FORMAT : ${{ inputs.report_format }}
146+ INPUT_REDACT : ${{ inputs.redact }}
147+ INPUT_EXIT_CODE : ${{ inputs.exit_code_on_leak }}
148+ CHANGED_ALL : ${{ steps.changed-files.outputs.all_changed_files }}
149+ run : |
150+ set -euo pipefail
151+
152+ mkdir -p security-results/gitleaks
153+ RAND_SUFFIX=$(head /dev/urandom | tr -dc a-z0-9 | head -c 6)
154+ REPORT_FILE="security-results/gitleaks/gitleaks-results-${RAND_SUFFIX}.${INPUT_FORMAT}"
155+ echo "rand_suffix=${RAND_SUFFIX}" >> "$GITHUB_OUTPUT"
156+
157+ # Build scan directory depending on scope
158+ SCAN_DIR="${INPUT_SOURCE}"
159+ if [[ "$INPUT_SCOPE" == "changed" ]]; then
160+ if [[ -n "${CHANGED_ALL:-}" ]]; then
161+ echo "Scanning only changed files"
162+ TMPDIR="$(mktemp -d)"
163+ # Recreate directory structure and copy only changed files
164+ while IFS= read -r f; do
165+ # Skip deleted files and ensure directory exists
166+ if [[ -f "$f" ]]; then
167+ mkdir -p "$TMPDIR/$(dirname "$f")"
168+ cp --parents "$f" "$TMPDIR" 2>/dev/null || cp "$f" "$TMPDIR/$(dirname "$f")/"
169+ fi
170+ done <<< "${CHANGED_ALL}"
171+ SCAN_DIR="$TMPDIR"
172+ NO_GIT="--no-git"
173+ else
174+ echo "No changed files detected; scanning full source"
175+ NO_GIT=""
176+ fi
177+ fi
178+
179+ # Build CLI
180+ CMD=(
181+ gitleaks detect
182+ --source "$SCAN_DIR"
183+ --report-format "$INPUT_FORMAT"
184+ --report-path "$REPORT_FILE"
185+ --exit-code "$INPUT_EXIT_CODE"
186+ )
187+ [[ -n "${NO_GIT:-}" ]] && CMD+=( "$NO_GIT" )
188+ [[ "$INPUT_REDACT" == "true" ]] && CMD+=( --redact )
189+ [[ -n "$INPUT_CONFIG" && -f "$INPUT_CONFIG" ]] && CMD+=( --config "$INPUT_CONFIG" )
190+ [[ -n "$INPUT_BASELINE" && -f "$INPUT_BASELINE" ]] && CMD+=( --baseline-path "$INPUT_BASELINE" )
191+
192+ echo "Executing: ${CMD[*]}"
193+ set +e
194+ "${CMD[@]}"
195+ STATUS=$?
196+ set -e
197+
198+ if [[ -f "$REPORT_FILE" ]]; then
199+ echo "report_path=$REPORT_FILE" >> "$GITHUB_OUTPUT"
200+ else
201+ echo "::error::Report file was not generated"
202+ exit 1
203+ fi
204+ echo "exit_code=$STATUS" >> "$GITHUB_OUTPUT"
205+ # Don't hard-fail the job; let the caller decide based on outputs.exit_code
206+ if [[ "$STATUS" -ne 0 ]]; then
207+ echo "::warning::Gitleaks detected leaks (exit code $STATUS)"
208+ fi
209+
210+ - name : Upload report artifact
211+ if : steps.run-gitleaks.outputs.report_path != ''
212+ env :
213+ suffix : ${{ inputs.report_suffix }}
214+ uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
215+ with :
216+ name : gitleaks-results-${{ steps.run-gitleaks.outputs.rand_suffix }}${{ env.suffix }}
217+ path : ${{ steps.run-gitleaks.outputs.report_path }}
218+ retention-days : 7
219+
220+ - name : Upload SARIF (code scanning)
221+ if : contains(steps.run-gitleaks.outputs.report_path, '.sarif')
222+ uses : github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.8
223+ with :
224+ sarif_file : ${{ steps.run-gitleaks.outputs.report_path }}
0 commit comments