Skip to content

Commit 1987547

Browse files
coopernetesclaude
andcommitted
feat: add secret scanning via gitleaks with hermetic bundling
- Wire SecretScanningFilter (proxy) and SecretScanningHook (S&F) using shared GitleaksRunner helper; both fail-open if scanner unavailable - Bundle gitleaks binary in JAR via Gradle downloadGitleaks task (detects build platform OS/arch automatically) - Runtime resolution order: scanner-path > version-pinned download > bundled JAR binary > system PATH - Add SecretScanningConfig to CommitConfig: enabled, auto-install, version, install-dir, scanner-path, config-file, timeout-seconds - Default: bundled DEFAULT_VERSION (8.21.2) used when enabled; set version + auto-install: true to download a different version at startup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5041114 commit 1987547

9 files changed

Lines changed: 812 additions & 44 deletions

File tree

jgit-proxy-core/build.gradle

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ plugins {
55
group = 'org.finos.gitproxy'
66
version = '0.0.1-SNAPSHOT'
77

8+
// Gitleaks version bundled into the JAR for hermetic secret scanning
9+
ext.gitleaksVersion = '8.21.2'
10+
811
java {
912
sourceCompatibility = JavaVersion.VERSION_21
1013
targetCompatibility = JavaVersion.VERSION_21
@@ -106,3 +109,94 @@ dependencies {
106109
tasks.named('test') {
107110
useJUnitPlatform()
108111
}
112+
113+
// ---------------------------------------------------------------------------
114+
// Hermetic gitleaks bundling
115+
// ---------------------------------------------------------------------------
116+
// Detects the current build platform (OS + arch) at Gradle configuration time
117+
// and downloads the matching gitleaks binary, storing it as the plain name
118+
// "gitleaks" under build/generated-resources/gitleaks/ so it is packed into
119+
// the JAR as the classpath resource "gitleaks/gitleaks".
120+
//
121+
// At runtime, GitleaksRunner extracts it to a JVM temp directory (cleaned up
122+
// on JVM shutdown) and invokes it directly — no host installation required.
123+
//
124+
// If the build platform is not recognised (e.g. Windows), the download is
125+
// skipped and a warning is printed. In that case set
126+
// commit.secretScanning.scanner-path in git-proxy-local.yml to point at a
127+
// locally installed gitleaks binary.
128+
// ---------------------------------------------------------------------------
129+
130+
// Resource root: files here appear at the root of the classpath in the JAR.
131+
// The gitleaks binary lands at build/generated-resources/gitleaks
132+
// and is packed into the JAR as the classpath resource "gitleaks".
133+
def gitleaksResourceDir = layout.buildDirectory.dir("generated-resources")
134+
135+
sourceSets.main.resources.srcDir(gitleaksResourceDir)
136+
137+
// Detect build-time OS and CPU architecture to select the right gitleaks tarball
138+
def buildOs = System.properties['os.name'].toLowerCase()
139+
def buildArch = System.properties['os.arch'].toLowerCase()
140+
141+
def gitleaksTarSuffix = {
142+
String osKey = buildOs.contains('linux') ? 'linux'
143+
: (buildOs.contains('mac') || buildOs.contains('darwin')) ? 'darwin'
144+
: null
145+
String archKey = (buildArch.contains('amd64') || buildArch.contains('x86_64')) ? 'x64'
146+
: (buildArch.contains('aarch64') || buildArch.contains('arm64')) ? 'arm64'
147+
: null
148+
(osKey && archKey) ? "${osKey}_${archKey}" : null
149+
}()
150+
151+
tasks.register('downloadGitleaks') {
152+
group = 'build setup'
153+
description = "Downloads the gitleaks ${gitleaksVersion} binary for the current build platform."
154+
155+
// Output: build/generated-resources/gitleaks → JAR classpath: gitleaks
156+
def outputFile = gitleaksResourceDir.map { it.file("gitleaks") }
157+
outputs.file(outputFile)
158+
159+
doLast {
160+
if (!gitleaksTarSuffix) {
161+
logger.warn(
162+
"Unsupported build platform (os='${buildOs}', arch='${buildArch}') — " +
163+
"skipping bundled gitleaks download. Set commit.secretScanning.scanner-path " +
164+
"in git-proxy-local.yml to use a locally installed gitleaks binary.")
165+
return
166+
}
167+
168+
def out = outputFile.get().asFile
169+
if (out.exists()) {
170+
logger.lifecycle("gitleaks ${gitleaksVersion} (${gitleaksTarSuffix}) already bundled, skipping.")
171+
return
172+
}
173+
out.parentFile.mkdirs()
174+
175+
def tarName = "gitleaks_${gitleaksVersion}_${gitleaksTarSuffix}.tar.gz"
176+
def tarUrl = "https://github.com/gitleaks/gitleaks/releases/download/v${gitleaksVersion}/${tarName}"
177+
def tmpDir = layout.buildDirectory.dir("tmp/gitleaks").get().asFile
178+
tmpDir.mkdirs()
179+
def tarFile = new File(tmpDir, tarName)
180+
181+
logger.lifecycle("Downloading gitleaks ${gitleaksVersion} (${gitleaksTarSuffix})...")
182+
new URL(tarUrl).withInputStream { is -> tarFile.bytes = is.bytes }
183+
184+
// Extract just the gitleaks binary; Ant places it as out.parent/gitleaks == out
185+
ant.untar(src: tarFile.absolutePath, dest: out.parent, compression: 'gzip') {
186+
patternset { include(name: 'gitleaks') }
187+
}
188+
189+
if (!out.exists()) {
190+
throw new GradleException("gitleaks binary not found in ${tarName} after extraction")
191+
}
192+
out.setExecutable(true)
193+
logger.lifecycle("Bundled gitleaks at ${out} (${out.length()} bytes)")
194+
}
195+
}
196+
197+
// The JAR always ships with the bundled gitleaks binary (DEFAULT_VERSION).
198+
// At runtime, GitleaksRunner uses it unless:
199+
// - commit.secretScanning.scanner-path is set (explicit override), or
200+
// - commit.secretScanning.version is set to a different version and auto-install is true
201+
// (downloads and caches the requested version on first use).
202+
processResources.dependsOn('downloadGitleaks')

jgit-proxy-core/src/main/java/org/finos/gitproxy/config/CommitConfig.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,65 @@ public static class DiffConfig {
108108
private BlockConfig block = BlockConfig.builder().build();
109109
}
110110

111+
/** Configuration for secret scanning via gitleaks. */
112+
@Builder.Default
113+
private SecretScanningConfig secretScanning = SecretScanningConfig.builder().build();
114+
115+
/** Configuration for secret scanning via an external scanner (gitleaks). */
116+
@Data
117+
@Builder
118+
public static class SecretScanningConfig {
119+
120+
/** Enable or disable secret scanning. Disabled by default. */
121+
@Builder.Default
122+
private boolean enabled = false;
123+
124+
/**
125+
* When {@code true} (the default when scanning is enabled), gitleaks is downloaded automatically on first use
126+
* if it cannot be found via {@code scannerPath}, on the system PATH, or bundled in the JAR. The binary is
127+
* cached in {@code installDir} across restarts. Set to {@code false} to require an explicit installation.
128+
*/
129+
@Builder.Default
130+
private boolean autoInstall = true;
131+
132+
/**
133+
* Directory where gitleaks is cached when auto-installed. Defaults to {@code ~/.cache/jgit-proxy/gitleaks}. The
134+
* directory is created automatically if it does not exist.
135+
*/
136+
private String installDir;
137+
138+
/**
139+
* Version of gitleaks to download when auto-installing. Defaults to the version tested with this release of
140+
* jgit-proxy. Override to pin a specific version.
141+
*/
142+
private String version;
143+
144+
/**
145+
* Explicit path to a gitleaks binary. Bypasses all other resolution (PATH, JAR, auto-install). Useful for macOS
146+
* / Windows developer machines or controlled deployments that manage gitleaks externally.
147+
*/
148+
private String scannerPath;
149+
150+
/**
151+
* Path to a gitleaks TOML configuration file. When {@code null} or blank, gitleaks uses its built-in detection
152+
* rules. Set this to layer in org-specific patterns on top of the defaults. Example:
153+
*
154+
* <pre>
155+
* title = "my-org"
156+
* [extend]
157+
* useDefault = true
158+
* [[rules]]
159+
* id = "my-org-api-key"
160+
* regex = '''MY_ORG_[0-9A-Z]{32}'''
161+
* </pre>
162+
*/
163+
private String configFile;
164+
165+
/** Maximum seconds to wait for the gitleaks process before aborting (fail-open). */
166+
@Builder.Default
167+
private long timeoutSeconds = 30;
168+
}
169+
111170
/**
112171
* Create a default configuration with no restrictions.
113172
*

0 commit comments

Comments
 (0)