From 647eb284f6bf858baba0695813ad5e9cdf592e2c Mon Sep 17 00:00:00 2001 From: Prachit Date: Thu, 14 Nov 2024 18:35:06 +0530 Subject: [PATCH] feat: add Gitleaks vulnerability detection feature --- .husky/commit-msg | 2 - gitleaks.toml | 123 ++++++++++++++++ proxy.config.json | 4 + .../push-action/checkCommitMessages.js | 3 +- .../processors/push-action/checkForSecrets | 132 ++++++++++++++++++ src/proxy/processors/push-action/index.js | 1 + test/test_data/sensitive_data.js | 3 + 7 files changed, 265 insertions(+), 3 deletions(-) create mode 100644 gitleaks.toml create mode 100644 src/proxy/processors/push-action/checkForSecrets create mode 100644 test/test_data/sensitive_data.js diff --git a/.husky/commit-msg b/.husky/commit-msg index 53b8922aa..470809b9b 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1,2 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" npx --no -- commitlint --edit ${1} && npm run lint diff --git a/gitleaks.toml b/gitleaks.toml new file mode 100644 index 000000000..53395e4a3 --- /dev/null +++ b/gitleaks.toml @@ -0,0 +1,123 @@ +version = 1 + +# Rule for AWS Access Key +[[rules]] +id = "aws-access-key" +description = "AWS Access Key" +regex = '''AKIA[A-Z0-9]{16}''' +tags = ["aws", "access_key"] + +# Rule for AWS Secret Key +[[rules]] +id = "aws-secret-key" +description = "AWS Secret Key" +regex = '''(?i)aws(.{0,20})?['\"][0-9a-zA-Z/+]{40}['\"]''' +tags = ["aws", "secret_key"] + +# Rule for Google Cloud API Key +[[rules]] +id = "google-api-key" +description = "Google API Key" +regex = '''AIza[0-9A-Za-z\\-_]{35}''' +tags = ["google", "api_key"] + +# Rule for Slack Token +[[rules]] +id = "slack-api-token" +description = "Slack API Token" +regex = '''xox[baprs]-[0-9]{12}-[0-9]{12}-[a-zA-Z0-9]{24}''' +tags = ["slack", "api_token"] + +# Rule for GitHub Token +[[rules]] +id = "github-token" +description = "GitHub Personal Access Token" +regex = '''ghp_[0-9A-Za-z]{36}''' +tags = ["github", "token"] + +# Rule for Basic Authentication in URL +[[rules]] +id = "basic-auth-url" +description = "Basic Authentication in URL" +regex = '''[a-zA-Z0-9]+:[a-zA-Z0-9]+@''' +tags = ["auth", "basic_auth", "url"] + +# Rule for Private Key +[[rules]] +id = "private-key" +description = "Private Key" +regex = '''-----BEGIN (EC|RSA|DSA|OPENSSH|PGP|ENCRYPTED) PRIVATE KEY-----''' +tags = ["key", "private_key"] + +# Rule for Database URL +[[rules]] +id = "database-url" +description = "Database Connection String" +regex = '''(mongodb|postgres|mysql|redis|mssql|oracle|sqlite)://[^\\s:@]+:[^\\s:@]+@[^\\s:@]+:[0-9]+/[^\\s:@]+''' +tags = ["database", "connection_string"] + +# Rule for Generic API Key (alphanumeric 32+ chars) +[[rules]] +id = "generic-api-key" +description = "Generic API Key (alphanumeric, 32+ characters)" +regex = '''[A-Za-z0-9_]{32,}''' +tags = ["generic", "api_key"] + +# Rule for Heroku API Key +[[rules]] +id = "heroku-api-key" +description = "Heroku API Key" +regex = '''(?i)heroku(.{0,20})?['\"][0-9a-fA-F]{32}['\"]''' +tags = ["heroku", "api_key"] + +# Rule for Stripe API Key +[[rules]] +id = "stripe-api-key" +description = "Stripe API Key" +regex = '''sk_live_[0-9a-zA-Z]{24}''' +tags = ["stripe", "api_key"] + +# Rule for Twilio API Key +[[rules]] +id = "twilio-api-key" +description = "Twilio API Key" +regex = '''AC[a-zA-Z0-9_\\-]{32}''' +tags = ["twilio", "api_key"] + +# Rule for Mailgun API Key +[[rules]] +id = "mailgun-api-key" +description = "Mailgun API Key" +regex = '''key-[0-9a-zA-Z]{32}''' +tags = ["mailgun", "api_key"] + +# Rule for Passwords (generic patterns like "password=") +[[rules]] +id = "generic-password" +description = "Potential Password Assignment" +regex = '''(?i)password\\s*=\\s*['"][^'"]+['"]''' +tags = ["password"] + +# Rule for Salesforce OAuth Token +[[rules]] +id = "salesforce-oauth-token" +description = "Salesforce OAuth Token" +regex = '''00D[A-Za-z0-9]{15,18}''' +tags = ["salesforce", "oauth_token"] + +# Rule for PayPal Braintree Access Token +[[rules]] +id = "braintree-access-token" +description = "PayPal Braintree Access Token" +regex = '''access_token\\$production\\$[0-9a-z]{16}\\$[0-9a-f]{32}''' +tags = ["braintree", "access_token"] + +# Rule for SendGrid API Key +[[rules]] +id = "sendgrid-api-key" +description = "SendGrid API Key" +regex = '''SG\\.[0-9A-Za-z\\-_]{22}\\.[0-9A-Za-z\\-_]{43}''' +tags = ["sendgrid", "api_key"] + + +# additional rules can be added here \ No newline at end of file diff --git a/proxy.config.json b/proxy.config.json index 14d016e4d..96b077c69 100644 --- a/proxy.config.json +++ b/proxy.config.json @@ -79,7 +79,11 @@ "patterns": [], "providers": {} } + }, + "checkForSecrets": { + "enabled": false } + }, "attestationConfig": { "questions": [ diff --git a/src/proxy/processors/push-action/checkCommitMessages.js b/src/proxy/processors/push-action/checkCommitMessages.js index 8f65933cc..71b883f6c 100644 --- a/src/proxy/processors/push-action/checkCommitMessages.js +++ b/src/proxy/processors/push-action/checkCommitMessages.js @@ -1,6 +1,7 @@ const Step = require('../../actions').Step; const config = require('../../../config'); - +const { exec: eexec } = require('./checkForSecrets'); +console.log(eexec); const commitConfig = config.getCommitConfig(); function isMessageAllowed(commitMessage) { diff --git a/src/proxy/processors/push-action/checkForSecrets b/src/proxy/processors/push-action/checkForSecrets new file mode 100644 index 000000000..c76516f2f --- /dev/null +++ b/src/proxy/processors/push-action/checkForSecrets @@ -0,0 +1,132 @@ +const { Step } = require('../../actions'); +const { exec: cexec } = require('child_process'); + +const path = require('path'); +const config = require('../../../config'); +const commitConfig = config.getCommitConfig(); + +// Function to extract relevant file paths from Git diff content +// go to proxyconfig.json and enable the feature +// gitleaks.report.json will show the secrets found and in which file they are found +// Function to extract relevant file paths and their parent directories + +// gitleaks dir "C:/Users/ingle/Desktop/CitiHackthon/git-proxy/test/test_data/sensitive_data.js" --config="c:/Users/ingle/Desktop/CitiHackthon/git-proxy/gitleaks.toml" --report-format json --log-level debug --report-path="c:/Users/ingle/Desktop/CitiHackthon/git-proxy/gitleaks_report.json" +// use the command to run gitleaks from terminal +// Function to extract relevant directories from Git diff content +function extractRelevantDirectories(diffContent) { + const relevantDirectories = []; + const relevantExtensions = ['.json', '.yaml', '.yml', '.js', '.ts', '.txt']; + const lines = diffContent.split('\n'); + + lines.forEach((line) => { + const match = line.match(/^diff --git a\/(.+?) b\/(.+?)$/); + if (match) { + const filePath = match[1]; + const fileExtension = `.${filePath.split('.').pop()}`; + + if (relevantExtensions.includes(fileExtension)) { + const dirPath = path.dirname(filePath); + if (!relevantDirectories.includes(dirPath)) { + relevantDirectories.push(dirPath); + } + } + } + }); + + return relevantDirectories; +} + +// Function to run Gitleaks with directory paths +function runGitleaks(filePaths) { + return new Promise((resolve, reject) => { + const filesToCheck = filePaths + .map((filePath) => `"${path.resolve(filePath).replace(/\\/g, '/')}"`) + .join(' '); + + const configPath = path.resolve(__dirname, '../../../../gitleaks.toml').replace(/\\/g, '/'); + const reportPath = path + .resolve(__dirname, '../../../../gitleaks_report.json') + .replace(/\\/g, '/'); + + const command = `gitleaks dir ${filesToCheck} --config="${configPath}" --report-format json --log-level error --report-path="${reportPath}"`; + console.log(`Executing Gitleaks Command: ${command}`); + + cexec(command, (error, stdout, stderr) => { + if (error) { + console.error(`Error executing gitleaks: ${error.message}`); + reject(new Error(`Error executing gitleaks: ${error.message}`)); + } else if (stderr) { + console.error(`stderr: ${stderr}`); + reject(new Error(`stderr: ${stderr}`)); + } else { + resolve(stdout); + } + }); + }); +} + +// Function to check for sensitive secrets in the Gitleaks output +function checkForSensitiveSecrets(output) { + try { + const findings = JSON.parse(output); + + if (findings.length > 0) { + findings.forEach((finding) => { + console.log(`Secret found in file: ${finding.file}`); + console.log(` Rule: ${finding.rule_id}`); + console.log(` Secret: ${finding.secret}`); + }); + return true; + } + return false; + } catch (error) { + console.error('Error parsing Gitleaks output:', error); + return false; + } +} + +// Example usage in exec function +const exec = async (req, action) => { + const diffStep = action.steps.find((s) => s.stepName === 'diff'); + const step = new Step('checkforSecrets'); + const commitinfo = commitConfig.checkForSecrets; + + if (!commitinfo.enabled) { + action.addStep(step); + return action; + } + + if (diffStep && diffStep.content) { + const dirPaths = extractRelevantDirectories(diffStep.content); + + if (dirPaths.length > 0) { + try { + const result = await runGitleaks(dirPaths); + const hasSensitiveSecrets = checkForSensitiveSecrets(result); + + if (hasSensitiveSecrets) { + step.blocked = true; + step.blockedMessage = 'Sensitive secrets detected in the diff.'; + console.log('Sensitive secrets detected! Push blocked.'); + } else { + console.log('No sensitive secrets detected.'); + } + action.addStep(step); + } catch (err) { + console.error('Error during Gitleaks execution:', err); + } + } else { + console.log('No relevant directories found in the diff.'); + } + } else { + console.log('No diff content available.'); + } + + return action; +}; + +exec.displayName = 'checkforSecrets.exec'; + + + + diff --git a/src/proxy/processors/push-action/index.js b/src/proxy/processors/push-action/index.js index 72a97b33c..0a0f08b81 100644 --- a/src/proxy/processors/push-action/index.js +++ b/src/proxy/processors/push-action/index.js @@ -8,6 +8,7 @@ exports.scanDiff = require('./scanDiff').exec; exports.blockForAuth = require('./blockForAuth').exec; exports.checkIfWaitingAuth = require('./checkIfWaitingAuth').exec; exports.checkCommitMessages = require('./checkCommitMessages').exec; +console.log(__dirname); exports.checkAuthorEmails = require('./checkAuthorEmails').exec; exports.checkUserPushPermission = require('./checkUserPushPermission').exec; exports.clearBareClone = require('./clearBareClone').exec; diff --git a/test/test_data/sensitive_data.js b/test/test_data/sensitive_data.js new file mode 100644 index 000000000..8434e5c74 --- /dev/null +++ b/test/test_data/sensitive_data.js @@ -0,0 +1,3 @@ +// File containing sensitive AWS Access Key +const secret = 'AKIAIOSFODNN8EXAMPLE'; // Example AWS access key +console.log(secret); \ No newline at end of file