Introduction to GitLeaks
GitLeaks is a powerful secret scanning tool that helps detect hardcoded secrets like passwords, API keys, and tokens in git repositories. This comprehensive guide covers integrating GitLeaks functionality into Java applications for proactive secret detection.
Table of Contents
- GitLeaks Integration Approaches
- Java GitLeaks Client Implementation
- Custom Secret Detection Rules
- CI/CD Integration
- Real-time Monitoring
- Enterprise Security Scanner
GitLeaks Integration Approaches
1. Direct CLI Integration
2. Java Native Implementation
3. Hybrid Approach
Java GitLeaks Client Implementation
Maven Dependencies
<dependencies> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- HTTP Client --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> <version>5.2.1</version> </dependency> <!-- Git Integration --> <dependency> <groupId>org.eclipse.jgit</groupId> <artifactId>org.eclipse.jgit</artifactId> <version>6.6.0.202305301015-r</version> </dependency> <!-- YAML Processing --> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-yaml</artifactId> <version>2.15.2</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.7</version> </dependency> </dependencies>
Basic GitLeaks CLI Wrapper
package com.security.gitleaks;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
public class GitLeaksCLIWrapper {
private final String gitleaksPath;
private final Path configPath;
public GitLeaksCLIWrapper(String gitleaksPath) {
this.gitleaksPath = gitleaksPath;
this.configPath = Paths.get("gitleaks-config.yml");
createDefaultConfig();
}
public GitLeaksCLIWrapper(String gitleaksPath, String configPath) {
this.gitleaksPath = gitleaksPath;
this.configPath = Paths.get(configPath);
}
public ScanResult scanRepository(String repoPath) throws IOException, InterruptedException {
return scanRepository(repoPath, new ScanOptions());
}
public ScanResult scanRepository(String repoPath, ScanOptions options)
throws IOException, InterruptedException {
List<String> command = buildCommand(repoPath, options);
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.directory(new File(repoPath));
if (options.getOutputFile() != null) {
processBuilder.redirectOutput(new File(options.getOutputFile()));
} else {
processBuilder.redirectOutput(ProcessBuilder.Redirect.PIPE);
}
processBuilder.redirectError(ProcessBuilder.Redirect.PIPE);
Process process = processBuilder.start();
boolean completed = process.waitFor(options.getTimeout(), TimeUnit.SECONDS);
if (!completed) {
process.destroyForcibly();
throw new RuntimeException("GitLeaks scan timed out after " + options.getTimeout() + " seconds");
}
int exitCode = process.exitValue();
String errorOutput = readStream(process.getErrorStream());
if (exitCode != 0 && exitCode != 1) {
throw new RuntimeException("GitLeaks failed with exit code " + exitCode + ": " + errorOutput);
}
String output = options.getOutputFile() != null ?
Files.readString(Paths.get(options.getOutputFile())) :
readStream(process.getInputStream());
return parseScanResult(output, exitCode, errorOutput);
}
private List<String> buildCommand(String repoPath, ScanOptions options) {
List<String> command = new ArrayList<>();
command.add(gitleaksPath);
command.add("detect");
command.add("--source");
command.add(repoPath);
command.add("--config");
command.add(configPath.toString());
command.add("--verbose");
if (options.getOutputFile() != null) {
command.add("--report-format");
command.add("json");
command.add("--report-path");
command.add(options.getOutputFile());
}
if (options.isNoGit()) {
command.add("--no-git");
}
if (options.getBranch() != null) {
command.add("--branch");
command.add(options.getBranch());
}
if (options.isLogOpts()) {
command.add("--log-opts");
command.add("-p");
}
return command;
}
private String readStream(InputStream inputStream) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
StringBuilder output = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
return output.toString();
}
}
private ScanResult parseScanResult(String output, int exitCode, String errorOutput) {
ScanResult result = new ScanResult();
result.setExitCode(exitCode);
result.setRawOutput(output);
result.setErrorOutput(errorOutput);
// Parse JSON output if available
if (output.trim().startsWith("[")) {
try {
List<SecretFinding> findings = parseJsonFindings(output);
result.setFindings(findings);
} catch (Exception e) {
result.setParseError(e.getMessage());
}
}
return result;
}
private List<SecretFinding> parseJsonFindings(String jsonOutput) throws Exception {
// JSON parsing implementation
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
return Arrays.asList(mapper.readValue(jsonOutput, SecretFinding[].class));
}
private void createDefaultConfig() {
try {
String defaultConfig = """
title: "Gitleaks Config"
rules:
- id: "aws-access-token"
description: "AWS Access Key ID"
regex: '(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'
keywords:
- "AKIA"
- "A3T"
- id: "aws-secret-key"
description: "AWS Secret Access Key"
regex: '(?i)aws(.{0,20})?[''\\"][0-9a-zA-Z/+]{40}[''\\"]'
- id: "github-pat"
description: "GitHub Personal Access Token"
regex: '(?i)(github|gh|pat).{0,20}[''\\"][0-9a-zA-Z]{35,40}[''\\"]'
- id: "ssh-private-key"
description: "SSH Private Key"
regex: '-----BEGIN [A-Z]* PRIVATE KEY-----'
- id: "jwt-token"
description: "JWT Token"
regex: 'eyJ[a-zA-Z0-9]{10,}\\.[a-zA-Z0-9_-]{10,}\\.[a-zA-Z0-9_-]{10,}'
- id: "password-in-url"
description: "Password in URL"
regex: '(?i)(postgres|mysql|redis|mongodb)://[a-zA-Z0-9_]+:([^@]+)@'
captureGroup: 2
- id: "generic-api-key"
description: "Generic API Key"
regex: '(?i)(api[_-]?key|secret)[\\s]*[=:][\\s]*[''\\"]?([a-zA-Z0-9]{20,50})[''\\"]?'
captureGroup: 2
""";
Files.writeString(configPath, defaultConfig);
} catch (IOException e) {
throw new RuntimeException("Failed to create default config", e);
}
}
// Data classes
public static class ScanOptions {
private String outputFile;
private boolean noGit = false;
private String branch;
private boolean logOpts = false;
private int timeout = 300; // 5 minutes
// Getters and setters
public String getOutputFile() { return outputFile; }
public void setOutputFile(String outputFile) { this.outputFile = outputFile; }
public boolean isNoGit() { return noGit; }
public void setNoGit(boolean noGit) { this.noGit = noGit; }
public String getBranch() { return branch; }
public void setBranch(String branch) { this.branch = branch; }
public boolean isLogOpts() { return logOpts; }
public void setLogOpts(boolean logOpts) { this.logOpts = logOpts; }
public int getTimeout() { return timeout; }
public void setTimeout(int timeout) { this.timeout = timeout; }
}
public static class ScanResult {
private int exitCode;
private String rawOutput;
private String errorOutput;
private String parseError;
private List<SecretFinding> findings = new ArrayList<>();
// Getters and setters
public int getExitCode() { return exitCode; }
public void setExitCode(int exitCode) { this.exitCode = exitCode; }
public String getRawOutput() { return rawOutput; }
public void setRawOutput(String rawOutput) { this.rawOutput = rawOutput; }
public String getErrorOutput() { return errorOutput; }
public void setErrorOutput(String errorOutput) { this.errorOutput = errorOutput; }
public String getParseError() { return parseError; }
public void setParseError(String parseError) { this.parseError = parseError; }
public List<SecretFinding> getFindings() { return findings; }
public void setFindings(List<SecretFinding> findings) { this.findings = findings; }
public boolean hasFindings() {
return exitCode == 1 && !findings.isEmpty();
}
public boolean isSuccess() {
return exitCode == 0;
}
}
public static class SecretFinding {
private String description;
private String file;
private String commit;
private String line;
private String secret;
private String ruleId;
private Map<String, String> tags;
private String author;
private String email;
private String date;
// Getters and setters
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getFile() { return file; }
public void setFile(String file) { this.file = file; }
public String getCommit() { return commit; }
public void setCommit(String commit) { this.commit = commit; }
public String getLine() { return line; }
public void setLine(String line) { this.line = line; }
public String getSecret() { return secret; }
public void setSecret(String secret) { this.secret = secret; }
public String getRuleId() { return ruleId; }
public void setRuleId(String ruleId) { this.ruleId = ruleId; }
public Map<String, String> getTags() { return tags; }
public void setTags(Map<String, String> tags) { this.tags = tags; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getDate() { return date; }
public void setDate(String date) { this.date = date; }
@Override
public String toString() {
return String.format("Finding[rule=%s, file=%s, line=%s, desc=%s]",
ruleId, file, line, description);
}
}
}
Custom Secret Detection Rules
Advanced Rule Configuration
package com.security.gitleaks.rules;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import java.nio.file.*;
import java.util.*;
public class CustomRuleManager {
private final Path rulesDirectory;
private final ObjectMapper yamlMapper;
public CustomRuleManager(String rulesDirectory) {
this.rulesDirectory = Paths.get(rulesDirectory);
this.yamlMapper = new ObjectMapper(new YAMLFactory());
createDefaultRules();
}
public void createRule(CustomRule rule) throws Exception {
String filename = rule.getId() + ".yml";
Path rulePath = rulesDirectory.resolve(filename);
Map<String, Object> ruleConfig = new LinkedHashMap<>();
ruleConfig.put("id", rule.getId());
ruleConfig.put("description", rule.getDescription());
ruleConfig.put("regex", rule.getRegex());
if (rule.getKeywords() != null && !rule.getKeywords().isEmpty()) {
ruleConfig.put("keywords", rule.getKeywords());
}
if (rule.getCaptureGroup() > 0) {
ruleConfig.put("captureGroup", rule.getCaptureGroup());
}
if (rule.getTags() != null && !rule.getTags().isEmpty()) {
ruleConfig.put("tags", rule.getTags());
}
if (rule.getEntropy() > 0) {
ruleConfig.put("entropy", rule.getEntropy());
}
yamlMapper.writeValue(rulePath.toFile(), ruleConfig);
}
public String generateCompleteConfig() throws Exception {
Map<String, Object> config = new LinkedHashMap<>();
config.put("title", "Custom GitLeaks Configuration");
config.put("rules", loadAllRules());
Path configPath = rulesDirectory.resolve("complete-config.yml");
yamlMapper.writeValue(configPath.toFile(), config);
return configPath.toString();
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> loadAllRules() throws Exception {
List<Map<String, Object>> allRules = new ArrayList<>();
if (Files.exists(rulesDirectory)) {
Files.list(rulesDirectory)
.filter(path -> path.toString().endsWith(".yml"))
.forEach(path -> {
try {
Map<String, Object> rule = yamlMapper.readValue(path.toFile(), Map.class);
allRules.add(rule);
} catch (Exception e) {
System.err.println("Error loading rule from " + path + ": " + e.getMessage());
}
});
}
return allRules;
}
private void createDefaultRules() {
try {
// Java-specific secrets
createRule(new CustomRule(
"java-properties-password",
"Password in Java Properties File",
"(?i)(password|pwd|pass)[\\s]*[=:][\\s]*([^\\s]{4,})",
Arrays.asList("password", "pwd", "pass"),
2,
Map.of("language", "java", "filetype", "properties"),
0
));
createRule(new CustomRule(
"spring-datasource-password",
"Spring Datasource Password",
"spring\\.datasource\\.password[\\s]*[=:][\\s]*([^\\s]{4,})",
Arrays.asList("spring.datasource.password"),
1,
Map.of("framework", "spring", "language", "java"),
0
));
createRule(new CustomRule(
"jdbc-password-url",
"JDBC Password in URL",
"jdbc:[a-z]+://[^:]+:([^@]+)@",
Arrays.asList("jdbc:"),
1,
Map.of("language", "java", "protocol", "jdbc"),
0
));
createRule(new CustomRule(
"java-keystore-password",
"Java Keystore Password",
"(?i)(keyStorePassword|trustStorePassword)[\\s]*[=:][\\s]*['\\\"]([^'\\\"]{4,})['\\\"]",
Arrays.asList("keyStorePassword", "trustStorePassword"),
2,
Map.of("language", "java", "component", "keystore"),
0
));
// API Keys for common Java services
createRule(new CustomRule(
"stripe-api-key",
"Stripe API Key",
"(?i)stripe(.{0,20})?['\\\"](sk|pk)_(test|live)_[a-zA-Z0-9]{24}['\\\"]",
Arrays.asList("stripe"),
0,
Map.of("service", "stripe"),
0
));
createRule(new CustomRule(
"sendgrid-api-key",
"SendGrid API Key",
"SG\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9_-]{43}",
Arrays.asList("SG."),
0,
Map.of("service", "sendgrid"),
0
));
createRule(new CustomRule(
"slack-webhook",
"Slack Webhook URL",
"https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8}/[a-zA-Z0-9_]{24}",
Arrays.asList("hooks.slack.com"),
0,
Map.of("service", "slack"),
0
));
} catch (Exception e) {
throw new RuntimeException("Failed to create default rules", e);
}
}
public static class CustomRule {
private String id;
private String description;
private String regex;
private List<String> keywords;
private int captureGroup;
private Map<String, String> tags;
private double entropy;
public CustomRule(String id, String description, String regex,
List<String> keywords, int captureGroup,
Map<String, String> tags, double entropy) {
this.id = id;
this.description = description;
this.regex = regex;
this.keywords = keywords;
this.captureGroup = captureGroup;
this.tags = tags;
this.entropy = entropy;
}
// Getters and setters
public String getId() { return id; }
public String getDescription() { return description; }
public String getRegex() { return regex; }
public List<String> getKeywords() { return keywords; }
public int getCaptureGroup() { return captureGroup; }
public Map<String, String> getTags() { return tags; }
public double getEntropy() { return entropy; }
}
}
CI/CD Integration
GitHub Actions Integration
package com.security.gitleaks.ci;
import java.nio.file.*;
import java.util.*;
public class CICDIntegration {
public String generateGitHubActionsWorkflow(ScanConfig config) {
return String.format("""
name: Secret Scanning with GitLeaks
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 0 * * 0' # Weekly scan
jobs:
gitleaks-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download GitLeaks
run: |
wget https://github.com/gitleaks/gitleaks/releases/download/v%s/gitleaks_%s_linux_x64.tar.gz
tar -xzf gitleaks_%s_linux_x64.tar.gz
sudo mv gitleaks /usr/local/bin/
- name: Run GitLeaks Scan
run: |
gitleaks detect --source . --config %s --report-format json --report-path gitleaks-report.json
- name: Upload Report
uses: actions/upload-artifact@v3
if: always()
with:
name: gitleaks-report
path: gitleaks-report.json
- name: Fail if secrets found
if: steps.gitleaks.outcome == 'failure'
run: |
echo "❌ Secrets detected in codebase!"
echo "Please check the gitleaks-report.json for details"
exit 1
""",
config.getGitleaksVersion(),
config.getGitleaksVersion(),
config.getGitleaksVersion(),
config.getConfigPath()
);
}
public String generateGitLabCIConfig(ScanConfig config) {
return String.format("""
stages:
- security
secret_scan:
stage: security
image: gitleaks/gitleaks:%s
script:
- gitleaks detect --source . --config %s --report-format json --report-path gitleaks-report.json
artifacts:
paths:
- gitleaks-report.json
when: always
allow_failure: false
""",
config.getGitleaksVersion(),
config.getConfigPath()
);
}
public String generateJenkinsPipeline(ScanConfig config) {
return String.format("""
pipeline {
agent any
stages {
stage('Secret Scan') {
steps {
script {
sh '''
wget https://github.com/gitleaks/gitleaks/releases/download/v%s/gitleaks_%s_linux_x64.tar.gz
tar -xzf gitleaks_%s_linux_x64.tar.gz
./gitleaks detect --source . --config %s --verbose
'''
}
}
post {
always {
junit '**/gitleaks-report.xml'
}
}
}
}
}
""",
config.getGitleaksVersion(),
config.getGitleaksVersion(),
config.getGitleaksVersion(),
config.getConfigPath()
);
}
public void setupCICD(String projectPath, CICDPlatform platform, ScanConfig config) throws Exception {
String workflowContent = "";
String workflowPath = "";
switch (platform) {
case GITHUB_ACTIONS:
workflowContent = generateGitHubActionsWorkflow(config);
workflowPath = ".github/workflows/gitleaks-scan.yml";
break;
case GITLAB_CI:
workflowContent = generateGitLabCIConfig(config);
workflowPath = ".gitlab-ci.yml";
break;
case JENKINS:
workflowContent = generateJenkinsPipeline(config);
workflowPath = "Jenkinsfile";
break;
}
Path fullPath = Paths.get(projectPath, workflowPath);
Files.createDirectories(fullPath.getParent());
Files.writeString(fullPath, workflowContent);
System.out.println("Created CI/CD configuration at: " + fullPath);
}
public static class ScanConfig {
private String gitleaksVersion = "8.18.0";
private String configPath = "gitleaks-config.yml";
private boolean failOnSecrets = true;
private List<String> scanBranches = Arrays.asList("main", "develop");
// Getters and setters
public String getGitleaksVersion() { return gitleaksVersion; }
public void setGitleaksVersion(String gitleaksVersion) { this.gitleaksVersion = gitleaksVersion; }
public String getConfigPath() { return configPath; }
public void setConfigPath(String configPath) { this.configPath = configPath; }
public boolean isFailOnSecrets() { return failOnSecrets; }
public void setFailOnSecrets(boolean failOnSecrets) { this.failOnSecrets = failOnSecrets; }
public List<String> getScanBranches() { return scanBranches; }
public void setScanBranches(List<String> scanBranches) { this.scanBranches = scanBranches; }
}
public enum CICDPlatform {
GITHUB_ACTIONS, GITLAB_CI, JENKINS
}
}
Real-time Monitoring
Git Hook Integration
package com.security.gitleaks.hooks;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import java.nio.file.*;
import java.util.*;
public class GitHookManager {
private final Path hooksDirectory;
private final GitLeaksCLIWrapper gitleaks;
public GitHookManager(String repoPath, GitLeaksCLIWrapper gitleaks) {
this.hooksDirectory = Paths.get(repoPath, ".git", "hooks");
this.gitleaks = gitleaks;
}
public void installPreCommitHook() throws Exception {
String hookContent = """
#!/bin/bash
# GitLeaks Pre-commit Hook
# This hook prevents commits with sensitive information
echo "Running GitLeaks secret scan..."
# Run gitleaks on staged files
RESULT=$(gitleaks detect --source . --staged --config gitleaks-config.yml --verbose)
if [ $? -eq 1 ]; then
echo "❌ GitLeaks found secrets in your changes:"
echo "$RESULT"
echo ""
echo "Please remove the secrets before committing."
exit 1
else
echo "✅ No secrets detected. Proceeding with commit."
exit 0
fi
""";
Path preCommitHook = hooksDirectory.resolve("pre-commit");
Files.writeString(preCommitHook, hookContent);
preCommitHook.toFile().setExecutable(true);
System.out.println("Pre-commit hook installed at: " + preCommitHook);
}
public void installPrePushHook() throws Exception {
String hookContent = """
#!/bin/bash
# GitLeaks Pre-push Hook
# This hook prevents pushing secrets to remote
echo "Running GitLeaks secret scan before push..."
# Scan entire repository
RESULT=$(gitleaks detect --source . --config gitleaks-config.yml --verbose)
if [ $? -eq 1 ]; then
echo "❌ GitLeaks found secrets in repository:"
echo "$RESULT"
echo ""
echo "Please remove the secrets before pushing."
exit 1
else
echo "✅ No secrets detected. Proceeding with push."
exit 0
fi
""";
Path prePushHook = hooksDirectory.resolve("pre-push");
Files.writeString(prePushHook, hookContent);
prePushHook.toFile().setExecutable(true);
System.out.println("Pre-push hook installed at: " + prePushHook);
}
public void installPostCommitHook() throws Exception {
String hookContent = """
#!/bin/bash
# GitLeaks Post-commit Hook
# This hook scans after commit and reports findings
echo "Running GitLeaks post-commit scan..."
# Scan the last commit
gitleaks detect --source . --log-opts "-1" --config gitleaks-config.yml
if [ $? -eq 1 ]; then
echo "⚠️ GitLeaks found secrets in the last commit."
echo "Please consider amending the commit to remove secrets."
else
echo "✅ Last commit is clean."
fi
""";
Path postCommitHook = hooksDirectory.resolve("post-commit");
Files.writeString(postCommitHook, hookContent);
postCommitHook.toFile().setExecutable(true);
System.out.println("Post-commit hook installed at: " + postCommitHook);
}
public void scanStagedChanges(String repoPath) throws Exception {
GitLeaksCLIWrapper.ScanOptions options = new GitLeaksCLIWrapper.ScanOptions();
options.setNoGit(false);
options.setLogOpts(true);
GitLeaksCLIWrapper.ScanResult result = gitleaks.scanRepository(repoPath, options);
if (result.hasFindings()) {
System.out.println("Secrets found in staged changes:");
result.getFindings().forEach(finding ->
System.out.println(" - " + finding.getDescription() + " in " + finding.getFile()));
throw new SecurityException("Commit rejected: Secrets detected in staged changes");
}
}
public void scanCommitHistory(String repoPath, String since) throws Exception {
GitLeaksCLIWrapper.ScanOptions options = new GitLeaksCLIWrapper.ScanOptions();
options.setNoGit(false);
options.setLogOpts(true);
if (since != null) {
// Add since parameter to log-opts
options.setLogOpts(true);
}
GitLeaksCLIWrapper.ScanResult result = gitleaks.scanRepository(repoPath, options);
if (result.hasFindings()) {
System.out.println("Secrets found in commit history:");
result.getFindings().forEach(finding ->
System.out.println(" - " + finding.getDescription() +
" in " + finding.getFile() +
" (commit: " + finding.getCommit() + ")"));
}
}
}
Enterprise Security Scanner
Comprehensive Security Scanner
package com.security.gitleaks.enterprise;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
public class EnterpriseSecurityScanner {
private final GitLeaksCLIWrapper gitleaks;
private final CustomRuleManager ruleManager;
private final ObjectMapper jsonMapper;
private final ExecutorService executorService;
private final String outputDirectory;
public EnterpriseSecurityScanner(String gitleaksPath, String rulesDirectory, String outputDir) {
this.gitleaks = new GitLeaksCLIWrapper(gitleaksPath);
this.ruleManager = new CustomRuleManager(rulesDirectory);
this.jsonMapper = new ObjectMapper();
this.executorService = Executors.newFixedThreadPool(5);
this.outputDirectory = outputDir;
createOutputDirectory();
}
public ScanReport scanRepository(String repoUrl, ScanConfiguration config) throws Exception {
String repoPath = cloneRepository(repoUrl);
return performScan(repoPath, config);
}
public BatchScanReport scanMultipleRepositories(List<String> repoUrls, ScanConfiguration config) {
BatchScanReport batchReport = new BatchScanReport();
batchReport.setStartTime(LocalDateTime.now());
List<Future<ScanReport>> futures = new ArrayList<>();
for (String repoUrl : repoUrls) {
Future<ScanReport> future = executorService.submit(() -> {
try {
return scanRepository(repoUrl, config);
} catch (Exception e) {
ScanReport errorReport = new ScanReport();
errorReport.setRepositoryUrl(repoUrl);
errorReport.setStatus(ScanStatus.FAILED);
errorReport.setErrorMessage(e.getMessage());
return errorReport;
}
});
futures.add(future);
}
for (Future<ScanReport> future : futures) {
try {
ScanReport report = future.get(30, TimeUnit.MINUTES);
batchReport.getIndividualReports().add(report);
if (report.getStatus() == ScanStatus.SUCCESS_WITH_FINDINGS) {
batchReport.getRepositoriesWithFindings().add(repoUrl);
}
} catch (TimeoutException e) {
ScanReport timeoutReport = new ScanReport();
timeoutReport.setStatus(ScanStatus.TIMEOUT);
timeoutReport.setErrorMessage("Scan timed out after 30 minutes");
batchReport.getIndividualReports().add(timeoutReport);
} catch (Exception e) {
ScanReport errorReport = new ScanReport();
errorReport.setStatus(ScanStatus.FAILED);
errorReport.setErrorMessage(e.getMessage());
batchReport.getIndividualReports().add(errorReport);
}
}
batchReport.setEndTime(LocalDateTime.now());
generateBatchReport(batchReport);
return batchReport;
}
private ScanReport performScan(String repoPath, ScanConfiguration config) throws Exception {
ScanReport report = new ScanReport();
report.setRepositoryPath(repoPath);
report.setStartTime(LocalDateTime.now());
try {
// Generate custom config if needed
String configPath = config.isUseCustomRules() ?
ruleManager.generateCompleteConfig() : "gitleaks-config.yml";
GitLeaksCLIWrapper.ScanOptions options = new GitLeaksCLIWrapper.ScanOptions();
options.setOutputFile(Paths.get(outputDirectory,
"scan-" + System.currentTimeMillis() + ".json").toString());
options.setTimeout(config.getTimeoutMinutes() * 60);
GitLeaksCLIWrapper.ScanResult result = gitleaks.scanRepository(repoPath, options);
report.setScanResult(result);
report.setEndTime(LocalDateTime.now());
if (result.isSuccess()) {
report.setStatus(ScanStatus.SUCCESS);
} else if (result.hasFindings()) {
report.setStatus(ScanStatus.SUCCESS_WITH_FINDINGS);
report.setFindingsCount(result.getFindings().size());
} else {
report.setStatus(ScanStatus.FAILED);
report.setErrorMessage(result.getErrorOutput());
}
// Generate detailed report
generateDetailedReport(report, config);
} catch (Exception e) {
report.setStatus(ScanStatus.FAILED);
report.setErrorMessage(e.getMessage());
report.setEndTime(LocalDateTime.now());
}
return report;
}
private String cloneRepository(String repoUrl) throws Exception {
String repoName = extractRepoName(repoUrl);
Path repoPath = Paths.get(outputDirectory, "repos", repoName);
if (!Files.exists(repoPath)) {
Files.createDirectories(repoPath.getParent());
ProcessBuilder pb = new ProcessBuilder("git", "clone", repoUrl, repoPath.toString());
Process process = pb.start();
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("Failed to clone repository: " + repoUrl);
}
}
return repoPath.toString();
}
private String extractRepoName(String repoUrl) {
// Extract repository name from URL
String[] parts = repoUrl.split("/");
String lastPart = parts[parts.length - 1];
return lastPart.replace(".git", "");
}
private void generateDetailedReport(ScanReport report, ScanConfiguration config) throws Exception {
Map<String, Object> detailedReport = new LinkedHashMap<>();
detailedReport.put("scanId", UUID.randomUUID().toString());
detailedReport.put("repository", report.getRepositoryPath());
detailedReport.put("timestamp", report.getStartTime().toString());
detailedReport.put("duration",
java.time.Duration.between(report.getStartTime(), report.getEndTime()).toString());
detailedReport.put("status", report.getStatus().toString());
if (report.getScanResult() != null && report.getScanResult().hasFindings()) {
List<Map<String, Object>> findingsSummary = new ArrayList<>();
// Group findings by type
Map<String, Long> findingsByType = report.getScanResult().getFindings().stream()
.collect(Collectors.groupingBy(
GitLeaksCLIWrapper.SecretFinding::getRuleId,
Collectors.counting()
));
findingsByType.forEach((ruleId, count) -> {
Map<String, Object> summary = new HashMap<>();
summary.put("ruleId", ruleId);
summary.put("count", count);
summary.put("severity", calculateSeverity(ruleId));
findingsSummary.add(summary);
});
detailedReport.put("findingsSummary", findingsSummary);
detailedReport.put("totalFindings", report.getFindingsCount());
// Sample of actual findings (limit to 10 for report)
List<Map<String, Object>> sampleFindings = report.getScanResult().getFindings().stream()
.limit(10)
.map(this::convertFindingToMap)
.toList();
detailedReport.put("sampleFindings", sampleFindings);
}
if (report.getErrorMessage() != null) {
detailedReport.put("error", report.getErrorMessage());
}
String reportFilename = String.format("detailed-report-%s.json",
report.getStartTime().format(java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME)
.replace(":", "-"));
Path reportPath = Paths.get(outputDirectory, "reports", reportFilename);
Files.createDirectories(reportPath.getParent());
jsonMapper.writerWithDefaultPrettyPrinter().writeValue(reportPath.toFile(), detailedReport);
report.setDetailedReportPath(reportPath.toString());
}
private void generateBatchReport(BatchScanReport batchReport) {
try {
Map<String, Object> batchSummary = new LinkedHashMap<>();
batchSummary.put("batchId", UUID.randomUUID().toString());
batchSummary.put("scanStart", batchReport.getStartTime().toString());
batchSummary.put("scanEnd", batchReport.getEndTime().toString());
batchSummary.put("totalRepositories", batchReport.getIndividualReports().size());
long successfulScans = batchReport.getIndividualReports().stream()
.filter(r -> r.getStatus() == ScanStatus.SUCCESS ||
r.getStatus() == ScanStatus.SUCCESS_WITH_FINDINGS)
.count();
long scansWithFindings = batchReport.getIndividualReports().stream()
.filter(r -> r.getStatus() == ScanStatus.SUCCESS_WITH_FINDINGS)
.count();
batchSummary.put("successfulScans", successfulScans);
batchSummary.put("scansWithFindings", scansWithFindings);
batchSummary.put("failedScans", batchReport.getIndividualReports().size() - successfulScans);
batchSummary.put("repositoriesWithFindings", batchReport.getRepositoriesWithFindings());
String batchReportFilename = String.format("batch-report-%s.json",
batchReport.getStartTime().format(java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME)
.replace(":", "-"));
Path batchReportPath = Paths.get(outputDirectory, "batch-reports", batchReportFilename);
Files.createDirectories(batchReportPath.getParent());
jsonMapper.writerWithDefaultPrettyPrinter().writeValue(batchReportPath.toFile(), batchSummary);
batchReport.setBatchReportPath(batchReportPath.toString());
} catch (Exception e) {
System.err.println("Failed to generate batch report: " + e.getMessage());
}
}
private String calculateSeverity(String ruleId) {
// Define severity based on rule type
if (ruleId.contains("password") || ruleId.contains("secret") || ruleId.contains("private-key")) {
return "HIGH";
} else if (ruleId.contains("api-key") || ruleId.contains("token")) {
return "MEDIUM";
} else {
return "LOW";
}
}
private Map<String, Object> convertFindingToMap(GitLeaksCLIWrapper.SecretFinding finding) {
Map<String, Object> map = new HashMap<>();
map.put("ruleId", finding.getRuleId());
map.put("description", finding.getDescription());
map.put("file", finding.getFile());
map.put("line", finding.getLine());
map.put("commit", finding.getCommit() != null ?
finding.getCommit().substring(0, 8) : "N/A");
map.put("author", finding.getAuthor());
map.put("date", finding.getDate());
return map;
}
private void createOutputDirectory() {
try {
Files.createDirectories(Paths.get(outputDirectory));
Files.createDirectories(Paths.get(outputDirectory, "repos"));
Files.createDirectories(Paths.get(outputDirectory, "reports"));
Files.createDirectories(Paths.get(outputDirectory, "batch-reports"));
} catch (Exception e) {
throw new RuntimeException("Failed to create output directory", e);
}
}
public void shutdown() {
executorService.shutdown();
try {
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
// Data classes
public static class ScanConfiguration {
private boolean useCustomRules = true;
private int timeoutMinutes = 10;
private boolean generateDetailedReport = true;
private List<String> excludedPaths = new ArrayList<>();
// Getters and setters
public boolean isUseCustomRules() { return useCustomRules; }
public void setUseCustomRules(boolean useCustomRules) { this.useCustomRules = useCustomRules; }
public int getTimeoutMinutes() { return timeoutMinutes; }
public void setTimeoutMinutes(int timeoutMinutes) { this.timeoutMinutes = timeoutMinutes; }
public boolean isGenerateDetailedReport() { return generateDetailedReport; }
public void setGenerateDetailedReport(boolean generateDetailedReport) { this.generateDetailedReport = generateDetailedReport; }
public List<String> getExcludedPaths() { return excludedPaths; }
public void setExcludedPaths(List<String> excludedPaths) { this.excludedPaths = excludedPaths; }
}
public static class ScanReport {
private String repositoryUrl;
private String repositoryPath;
private LocalDateTime startTime;
private LocalDateTime endTime;
private ScanStatus status;
private String errorMessage;
private GitLeaksCLIWrapper.ScanResult scanResult;
private int findingsCount;
private String detailedReportPath;
// Getters and setters
public String getRepositoryUrl() { return repositoryUrl; }
public void setRepositoryUrl(String repositoryUrl) { this.repositoryUrl = repositoryUrl; }
public String getRepositoryPath() { return repositoryPath; }
public void setRepositoryPath(String repositoryPath) { this.repositoryPath = repositoryPath; }
public LocalDateTime getStartTime() { return startTime; }
public void setStartTime(LocalDateTime startTime) { this.startTime = startTime; }
public LocalDateTime getEndTime() { return endTime; }
public void setEndTime(LocalDateTime endTime) { this.endTime = endTime; }
public ScanStatus getStatus() { return status; }
public void setStatus(ScanStatus status) { this.status = status; }
public String getErrorMessage() { return errorMessage; }
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
public GitLeaksCLIWrapper.ScanResult getScanResult() { return scanResult; }
public void setScanResult(GitLeaksCLIWrapper.ScanResult scanResult) { this.scanResult = scanResult; }
public int getFindingsCount() { return findingsCount; }
public void setFindingsCount(int findingsCount) { this.findingsCount = findingsCount; }
public String getDetailedReportPath() { return detailedReportPath; }
public void setDetailedReportPath(String detailedReportPath) { this.detailedReportPath = detailedReportPath; }
}
public static class BatchScanReport {
private LocalDateTime startTime;
private LocalDateTime endTime;
private List<ScanReport> individualReports = new ArrayList<>();
private List<String> repositoriesWithFindings = new ArrayList<>();
private String batchReportPath;
// Getters and setters
public LocalDateTime getStartTime() { return startTime; }
public void setStartTime(LocalDateTime startTime) { this.startTime = startTime; }
public LocalDateTime getEndTime() { return endTime; }
public void setEndTime(LocalDateTime endTime) { this.endTime = endTime; }
public List<ScanReport> getIndividualReports() { return individualReports; }
public void setIndividualReports(List<ScanReport> individualReports) { this.individualReports = individualReports; }
public List<String> getRepositoriesWithFindings() { return repositoriesWithFindings; }
public void setRepositoriesWithFindings(List<String> repositoriesWithFindings) { this.repositoriesWithFindings = repositoriesWithFindings; }
public String getBatchReportPath() { return batchReportPath; }
public void setBatchReportPath(String batchReportPath) { this.batchReportPath = batchReportPath; }
}
public enum ScanStatus {
SUCCESS, SUCCESS_WITH_FINDINGS, FAILED, TIMEOUT
}
}
Usage Examples
Basic Usage
public class GitLeaksExample {
public static void main(String[] args) {
try {
// Initialize GitLeaks wrapper
GitLeaksCLIWrapper gitleaks = new GitLeaksCLIWrapper("/usr/local/bin/gitleaks");
// Scan a repository
GitLeaksCLIWrapper.ScanResult result = gitleaks.scanRepository("/path/to/repo");
if (result.hasFindings()) {
System.out.println("Found secrets:");
result.getFindings().forEach(finding ->
System.out.println(" - " + finding.getDescription() + " in " + finding.getFile()));
} else {
System.out.println("No secrets found!");
}
// Enterprise scanning
EnterpriseSecurityScanner scanner = new EnterpriseSecurityScanner(
"/usr/local/bin/gitleaks",
"./custom-rules",
"./scan-results"
);
List<String> repositories = Arrays.asList(
"https://github.com/company/repo1.git",
"https://github.com/company/repo2.git"
);
EnterpriseSecurityScanner.ScanConfiguration config =
new EnterpriseSecurityScanner.ScanConfiguration();
config.setUseCustomRules(true);
config.setTimeoutMinutes(15);
EnterpriseSecurityScanner.BatchScanReport batchReport =
scanner.scanMultipleRepositories(repositories, config);
System.out.println("Batch scan completed: " +
batchReport.getIndividualReports().size() + " repositories scanned");
scanner.shutdown();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Best Practices
- Regular Scanning: Schedule daily or weekly scans of all repositories
- Custom Rules: Develop organization-specific detection rules
- Pre-commit Hooks: Prevent secrets from being committed in the first place
- CI/CD Integration: Automate scanning in your pipeline
- Remediation Workflow: Establish processes for fixing found secrets
- Monitoring and Alerting: Set up alerts for critical findings
This comprehensive GitLeaks integration provides enterprise-grade secret scanning capabilities for Java applications, helping to prevent security breaches caused by accidental secret exposure.