Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
repos:
- repo: local
hooks:
- id: cyberferret
name: CyberFerret Sensitive Data Scanner
language: script
entry: hooks/cyberferret.sh
pass_filenames: false
stages: [pre-commit]
10 changes: 10 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
- id: cyberferret
name: CyberFerret Sensitive Data Scanner
description: >
Downloads the latest CyberFerret CLI jar from https://github.com/exadmin/CyberFerret
and scans staged files for sensitive data (passwords, tokens, keys, etc.).
Requires CYBER_FERRET_PASSWORD environment variable to be set.
language: script
entry: hooks/cyberferret.sh
pass_filenames: false
stages: [pre-commit]
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,9 @@ private static void _main(String[] args) {
RunnableSigsLoader sigsLoader = new RunnableSigsLoader(true);
sigsLoader.setPrintToConsole(true);
try {
String prefix = GitUtils.getGlobalConfigValue("core.hooksPath");
if (MiscUtils.isEmpty(prefix)) {
ConsoleUtils.error("Global hooksPath is not empty");
terminateAppWithErrorCode(false);
}
Path path = Paths.get(prefix, AppConstants.DICTIONARY_FILE_PATH_ENCRYPTED);
String prefix = GitUtils.getConfigValue("core.hooksPath");
if (prefix == null) prefix = "";
Path path = Paths.get(prefix.isEmpty() ? "." : prefix, AppConstants.DICTIONARY_FILE_PATH_ENCRYPTED);
String encryptedBody = FileUtils.readFile(path);
String decryptedBody = PasswordBasedEncryption.decrypt(encryptedBody, pass);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ protected void _run() {
logInfo("Checking if new online dictionary exists");

String prefix = "";
if (isCLIMode()) prefix = GitUtils.getGlobalConfigValue("core.hooksPath");
if (isCLIMode()) prefix = GitUtils.getConfigValue("core.hooksPath");
if (prefix == null) prefix = "";
Path path = Paths.get(prefix, AppConstants.DICTIONARY_FILE_PATH_ENCRYPTED);
File savePath = path.toFile();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;

Expand Down Expand Up @@ -67,6 +68,36 @@ public static String getGlobalConfigValue(String key) {
return null;
}

/**
* Returns the value of a git config key by delegating to the {@code git config} process,
* which respects the full resolution order: system → global (with conditional includes) → local.
*
* @param key config key in "section.property" format
* @return trimmed value, or {@code null} if not set or git is unavailable
*/
public static String getConfigValue(String key) {
if (key == null || !key.contains(".")) {
throw new IllegalArgumentException("Key must have format 'section.property'");
}
try {
Process process = new ProcessBuilder("git", "config", key)
.redirectErrorStream(false)
.start();
String value;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
value = reader.readLine();
}
int exitCode = process.waitFor();
if (exitCode != 0) {
return null;
}
return (value != null) ? value.trim() : null;
} catch (IOException | InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}

private static Path findGlobalGitConfig() {

String xdg = System.getenv("XDG_CONFIG_HOME");
Expand Down
77 changes: 77 additions & 0 deletions hooks/cyberferret.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# CyberFerret pre-commit framework entry point.
#
# Required environment variable:
# CYBER_FERRET_PASSWORD – decryption password for the signature dictionary

set -euo pipefail

# ── OS-specific cache directory ────────────────────────────────────────────────
case "$(uname -s 2>/dev/null || echo Unknown)" in
Linux*) CF_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/CyberFerret" ;;
Darwin*) CF_CACHE_DIR="$HOME/Library/Caches/CyberFerret" ;;
CYGWIN*|MINGW*|MSYS*) CF_CACHE_DIR="${LOCALAPPDATA:-${APPDATA:-$HOME/AppData/Local}}/CyberFerret" ;;
*) CF_CACHE_DIR="$HOME/.cache/CyberFerret" ;;
esac

mkdir -p "$CF_CACHE_DIR"

# ── Download latest jar if a newer release is available ───────────────────────
JAR_PATH="$CF_CACHE_DIR/cyberferret-cli.jar"
VERSION_FILE="$CF_CACHE_DIR/.cyberferret-version"
GITHUB_API_URL="https://api.github.com/repos/exadmin/CyberFerret/releases/latest"

RELEASE_JSON="$(curl -sf --connect-timeout 10 "$GITHUB_API_URL")" \
|| { echo "[CyberFerret] ERROR: cannot reach GitHub API" >&2; exit 1; }

# Use python3 for reliable JSON parsing; fall back to grep/sed if unavailable.
if command -v python3 >/dev/null 2>&1; then
LATEST_TAG="$(printf '%s' "$RELEASE_JSON" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')"
JAR_URL="$(printf '%s' "$RELEASE_JSON" \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(next(a["browser_download_url"] for a in d["assets"] if a["name"]=="cyberferret-cli.jar"))')"
else
LATEST_TAG="$(printf '%s' "$RELEASE_JSON" \
| grep '"tag_name"' | head -1 \
| sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
JAR_URL="$(printf '%s' "$RELEASE_JSON" \
| grep '"browser_download_url"' | grep 'cyberferret-cli\.jar' | head -1 \
| sed 's/.*"browser_download_url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
fi

[ -n "$LATEST_TAG" ] || { echo "[CyberFerret] ERROR: failed to parse release tag" >&2; exit 1; }
[ -n "$JAR_URL" ] || { echo "[CyberFerret] ERROR: cyberferret-cli.jar asset not found in release $LATEST_TAG" >&2; exit 1; }

CACHED_TAG="$(cat "$VERSION_FILE" 2>/dev/null || true)"
if [ ! -f "$JAR_PATH" ] || [ "$CACHED_TAG" != "$LATEST_TAG" ]; then
echo "[CyberFerret] Downloading $LATEST_TAG ..." >&2
_tmp="${JAR_PATH}.tmp.$$"
if curl -fL --connect-timeout 60 -o "$_tmp" "$JAR_URL"; then
mv "$_tmp" "$JAR_PATH"
printf '%s' "$LATEST_TAG" > "$VERSION_FILE"
echo "[CyberFerret] Download complete." >&2
else
rm -f "$_tmp"
echo "[CyberFerret] ERROR: jar download failed" >&2
exit 1
fi
fi

# ── Build staged-file list ─────────────────────────────────────────────────────
REPO_ROOT="$(git rev-parse --show-toplevel)"
FILES_LIST="$(mktemp)"
trap 'rm -f "$FILES_LIST"' EXIT

# Pass relative paths – CyberFerretCLI resolves them against REPO_ROOT.
git diff --cached --name-only > "$FILES_LIST"

if [ ! -s "$FILES_LIST" ]; then
echo "[CyberFerret] No staged files to scan." >&2
exit 0
fi

# ── Run CyberFerret (PWD = cache dir so all temp/cache files land there) ──────
cd "$CF_CACHE_DIR"
exec java -cp "$JAR_PATH" \
com.github.exadmin.cyberferret.CyberFerretCLI \
"$REPO_ROOT" "$FILES_LIST"
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<name>cyber-ferret</name>

<properties>
<revision>1.2.3</revision>
<revision>1.2.4</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.10.2</junit.version>
<maven.compiler.source>21</maven.compiler.source>
Expand Down
Loading