Skip to content

Commit 0a3f653

Browse files
coopernetesclaude
andcommitted
feat: bundle gitleaks for all supported architectures
Bumps gitleaks from 8.21.2 to 8.30.1 and bundles all four supported binaries (linux_x64, linux_arm64, darwin_x64, darwin_arm64) as arch-specific classpath resources. GitleaksRunner.extractBundledBinary() now selects the resource matching the runtime platform, fixing the silent fail-open (or fail-closed) breakage on arm64 containers that previously received a build-machine binary they could not execute. Docker builds pass -PgitleaksTargets=linux_{x64,arm64} derived from TARGETARCH so each arch-specific image carries only its own binary; local/dev builds bundle all four. closes #201 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 15d500e commit 0a3f653

4 files changed

Lines changed: 89 additions & 77 deletions

File tree

Dockerfile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,17 @@ COPY . .
3535
# Build the distribution (all deps bundled in lib/).
3636
# Node.js is installed above; the node-gradle plugin uses it from PATH (download=false).
3737
# BuildKit cache mounts persist Gradle/npm downloads across builds.
38+
# gitleaksTargets is derived from TARGETARCH so only the matching binary is bundled,
39+
# keeping each arch-specific image lean (amd64 image carries only linux_x64, etc.).
3840
RUN --mount=type=cache,target=/root/.gradle/caches \
3941
--mount=type=cache,target=/root/.gradle/wrapper \
4042
--mount=type=cache,target=/root/.npm \
41-
./gradlew clean :git-proxy-java-dashboard:installDist --no-daemon -q
43+
case "${TARGETARCH}" in \
44+
arm64) GITLEAKS_TARGET=linux_arm64 ;; \
45+
*) GITLEAKS_TARGET=linux_x64 ;; \
46+
esac \
47+
&& ./gradlew clean :git-proxy-java-dashboard:installDist \
48+
-PgitleaksTargets=${GITLEAKS_TARGET} --no-daemon -q
4249

4350
# Prepend a conf/ directory to the classpath so that a mounted git-proxy-local.yml
4451
# is picked up by JettyConfigurationLoader at runtime.

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ ext {
6868
testContainersVersion = '2.0.4'
6969

7070
// Gitleaks (bundled binary)
71-
gitleaksVersion = '8.21.2'
71+
gitleaksVersion = '8.30.1'
7272

7373
// OpenAPI spec generation
7474
swaggerCoreVersion = '2.2.48'

git-proxy-java-core/build.gradle

Lines changed: 62 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -189,95 +189,88 @@ jacocoTestCoverageVerification {
189189
// ---------------------------------------------------------------------------
190190
// Hermetic gitleaks bundling
191191
// ---------------------------------------------------------------------------
192-
// Detects the current build platform (OS + arch) at Gradle configuration time
193-
// and downloads the matching gitleaks binary, storing it as the plain name
194-
// "gitleaks" under build/generated-resources/gitleaks/ so it is packed into
195-
// the JAR as the classpath resource "gitleaks/gitleaks".
192+
// Downloads gitleaks binaries at build time and packs them into the JAR as
193+
// arch-specific classpath resources so GitleaksRunner can extract the right
194+
// one at runtime without a host installation.
196195
//
197-
// At runtime, GitleaksRunner extracts it to a JVM temp directory (cleaned up
198-
// on JVM shutdown) and invokes it directly — no host installation required.
199-
//
200-
// If the build platform is not recognised (e.g. Windows), the download is
201-
// skipped and a warning is printed. In that case set
202-
// commit.secretScanning.scanner-path in git-proxy-local.yml to point at a
203-
// locally installed gitleaks binary.
196+
// Default: auto-detects the host OS/arch and downloads only that binary.
197+
// Override via -PgitleaksTargets to download specific platforms:
198+
// -PgitleaksTargets=all → all four supported targets
199+
// -PgitleaksTargets=linux_x64,linux_arm64 → explicit list
200+
// -PgitleaksTargets=linux_x64 → single target (used by Dockerfile)
204201
// ---------------------------------------------------------------------------
205202

206-
// Resource root: files here appear at the root of the classpath in the JAR.
207-
// The gitleaks binary lands at build/generated-resources/gitleaks
208-
// and is packed into the JAR as the classpath resource "gitleaks".
209203
def gitleaksResourceDir = layout.buildDirectory.dir("generated-resources")
210-
211204
sourceSets.main.resources.srcDir(gitleaksResourceDir)
212205

213-
// Detect build-time OS and CPU architecture to select the right gitleaks tarball
214-
def buildOs = System.properties['os.name'].toLowerCase()
215-
def buildArch = System.properties['os.arch'].toLowerCase()
206+
def allGitleaksTargets = ['linux_x64', 'linux_arm64', 'darwin_x64', 'darwin_arm64']
216207

217-
def gitleaksTarSuffix = {
218-
String osKey = buildOs.contains('linux') ? 'linux'
219-
: (buildOs.contains('mac') || buildOs.contains('darwin')) ? 'darwin'
208+
def gitleaksTargets = {
209+
if (project.hasProperty('gitleaksTargets')) {
210+
String prop = project.getProperty('gitleaksTargets') as String
211+
return prop.trim() == 'all'
212+
? allGitleaksTargets
213+
: prop.split(',').collect { it.trim() }.toList()
214+
}
215+
// Default: detect host platform
216+
String os = System.properties['os.name'].toLowerCase()
217+
String arch = System.properties['os.arch'].toLowerCase()
218+
String osKey = os.contains('linux') ? 'linux'
219+
: (os.contains('mac') || os.contains('darwin')) ? 'darwin'
220220
: null
221-
String archKey = (buildArch.contains('amd64') || buildArch.contains('x86_64')) ? 'x64'
222-
: (buildArch.contains('aarch64') || buildArch.contains('arm64')) ? 'arm64'
221+
String archKey = (arch.contains('amd64') || arch.contains('x86_64')) ? 'x64'
222+
: (arch.contains('aarch64') || arch.contains('arm64')) ? 'arm64'
223223
: null
224-
(osKey && archKey) ? "${osKey}_${archKey}" : null
224+
if (osKey && archKey) return ["${osKey}_${archKey}"]
225+
logger.warn("gitleaks: unrecognised build platform (os='${os}', arch='${arch}') — " +
226+
"skipping download. Use -PgitleaksTargets=<target> or set scanner-path in config.")
227+
return []
225228
}()
226229

227230
tasks.register('downloadGitleaks') {
228231
group = 'build setup'
229-
description = "Downloads the gitleaks ${gitleaksVersion} binary for the current build platform."
232+
description = "Downloads gitleaks ${gitleaksVersion} binaries for all supported platforms."
230233

231-
// Output: build/generated-resources/gitleaks → JAR classpath: gitleaks
232-
def outputFile = gitleaksResourceDir.map { it.file("gitleaks") }
233-
outputs.file(outputFile)
234+
gitleaksTargets.each { suffix ->
235+
outputs.file(gitleaksResourceDir.map { it.file("gitleaks/${suffix}") })
236+
}
234237

235238
doLast {
236-
if (!gitleaksTarSuffix) {
237-
logger.warn(
238-
"Unsupported build platform (os='${buildOs}', arch='${buildArch}') — " +
239-
"skipping bundled gitleaks download. Set commit.secretScanning.scanner-path " +
240-
"in git-proxy-local.yml to use a locally installed gitleaks binary.")
241-
return
242-
}
243-
244-
def out = outputFile.get().asFile
245-
if (out.exists()) {
246-
logger.lifecycle("gitleaks ${gitleaksVersion} (${gitleaksTarSuffix}) already bundled, skipping.")
247-
return
248-
}
249-
out.parentFile.mkdirs()
250-
251-
def tarName = "gitleaks_${gitleaksVersion}_${gitleaksTarSuffix}.tar.gz"
252-
def gitleaksDownloadUrl = 'https://github.com/gitleaks/gitleaks/releases/download'
253-
if (System.getenv('GITLEAKS_DOWNLOAD_URL')) {
254-
gitleaksDownloadUrl = System.getenv('GITLEAKS_DOWNLOAD_URL')
255-
}
256-
def tarUrl = "${gitleaksDownloadUrl}/v${gitleaksVersion}/${tarName}"
257-
def tmpDir = layout.buildDirectory.dir("tmp/gitleaks").get().asFile
239+
def baseDownloadUrl = System.getenv('GITLEAKS_DOWNLOAD_URL')
240+
?: 'https://github.com/gitleaks/gitleaks/releases/download'
241+
def tmpDir = layout.buildDirectory.dir("tmp/gitleaks").get().asFile
258242
tmpDir.mkdirs()
259-
def tarFile = new File(tmpDir, tarName)
260-
261-
logger.lifecycle("Downloading gitleaks ${gitleaksVersion} (${gitleaksTarSuffix})...")
262-
new URL(tarUrl).withInputStream { is -> tarFile.bytes = is.bytes }
263-
264-
// Extract just the gitleaks binary; Ant places it as out.parent/gitleaks == out
265-
ant.untar(src: tarFile.absolutePath, dest: out.parent, compression: 'gzip') {
266-
patternset { include(name: 'gitleaks') }
267-
}
268243

269-
if (!out.exists()) {
270-
throw new GradleException("gitleaks binary not found in ${tarName} after extraction")
244+
gitleaksTargets.each { suffix ->
245+
def out = gitleaksResourceDir.get().file("gitleaks/${suffix}").asFile
246+
if (out.exists()) {
247+
logger.lifecycle("gitleaks ${gitleaksVersion} (${suffix}) already bundled, skipping.")
248+
return
249+
}
250+
out.parentFile.mkdirs()
251+
252+
def tarName = "gitleaks_${gitleaksVersion}_${suffix}.tar.gz"
253+
def tarUrl = "${baseDownloadUrl}/v${gitleaksVersion}/${tarName}"
254+
def tarFile = new File(tmpDir, tarName)
255+
256+
logger.lifecycle("Downloading gitleaks ${gitleaksVersion} (${suffix})...")
257+
new URL(tarUrl).withInputStream { is -> tarFile.bytes = is.bytes }
258+
259+
// Extract to a suffix-specific temp dir to avoid name collision between targets
260+
def extractDir = new File(tmpDir, suffix)
261+
extractDir.mkdirs()
262+
ant.untar(src: tarFile.absolutePath, dest: extractDir.absolutePath, compression: 'gzip') {
263+
patternset { include(name: 'gitleaks') }
264+
}
265+
def extracted = new File(extractDir, 'gitleaks')
266+
if (!extracted.exists()) {
267+
throw new GradleException("gitleaks binary not found in ${tarName} after extraction")
268+
}
269+
extracted.renameTo(out)
270+
out.setExecutable(true)
271+
logger.lifecycle("Bundled gitleaks (${suffix}) at ${out} (${out.length()} bytes)")
271272
}
272-
out.setExecutable(true)
273-
logger.lifecycle("Bundled gitleaks at ${out} (${out.length()} bytes)")
274273
}
275274
}
276275

277-
// The JAR always ships with the bundled gitleaks binary (DEFAULT_VERSION).
278-
// At runtime, GitleaksRunner uses it unless:
279-
// - commit.secretScanning.scanner-path is set (explicit override), or
280-
// - commit.secretScanning.version is set to a different version and auto-install is true
281-
// (downloads and caches the requested version on first use).
282-
// The download is a no-op when the binary is already present, so it only costs time once.
283276
processResources.dependsOn('downloadGitleaks')

git-proxy-java-core/src/main/java/org/finos/gitproxy/git/GitleaksRunner.java

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ public class GitleaksRunner {
5353
* Default gitleaks version used for auto-install. Keep in sync with {@code gitleaksVersion} in
5454
* {@code git-proxy-java-core/build.gradle}.
5555
*/
56-
public static final String DEFAULT_VERSION = "8.21.2";
56+
public static final String DEFAULT_VERSION = "8.30.1";
5757

58-
/** Classpath resource name for the pre-bundled gitleaks binary (opt-in, not shipped by default). */
59-
private static final String BUNDLED_BINARY_RESOURCE = "gitleaks";
58+
/** Classpath resource prefix for bundled gitleaks binaries; full path is {@code gitleaks/<os>_<arch>}. */
59+
private static final String BUNDLED_BINARY_RESOURCE_PREFIX = "gitleaks/";
6060

6161
/** Exit code gitleaks returns when findings are present (distinct from error exit codes). */
6262
private static final int FINDINGS_EXIT_CODE = 2;
@@ -395,8 +395,20 @@ private Path extractBundledBinary() throws IOException {
395395
synchronized (EXTRACT_LOCK) {
396396
if (extractedBinaryPath != null) return extractedBinaryPath;
397397

398-
InputStream resource = GitleaksRunner.class.getClassLoader().getResourceAsStream(BUNDLED_BINARY_RESOURCE);
399-
if (resource == null) return null;
398+
String suffix = detectTarSuffix();
399+
if (suffix == null) {
400+
log.debug(
401+
"gitleaks: no bundled binary for platform ({} / {})",
402+
System.getProperty("os.name"),
403+
System.getProperty("os.arch"));
404+
return null;
405+
}
406+
String resourceName = BUNDLED_BINARY_RESOURCE_PREFIX + suffix;
407+
InputStream resource = GitleaksRunner.class.getClassLoader().getResourceAsStream(resourceName);
408+
if (resource == null) {
409+
log.debug("gitleaks bundled resource not found: {}", resourceName);
410+
return null;
411+
}
400412

401413
Path tempDir = Files.createTempDirectory("git-proxy-java-gitleaks-");
402414
Path binaryPath = tempDir.resolve("gitleaks");
@@ -414,7 +426,7 @@ private Path extractBundledBinary() throws IOException {
414426
}));
415427

416428
extractedBinaryPath = binaryPath;
417-
log.info("Extracted bundled gitleaks binary to {}", binaryPath);
429+
log.info("Extracted bundled gitleaks binary ({}) to {}", suffix, binaryPath);
418430
return extractedBinaryPath;
419431
}
420432
}

0 commit comments

Comments
 (0)