diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..4aec34a
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -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]
diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..16b988e
--- /dev/null
+++ b/.pre-commit-hooks.yaml
@@ -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]
diff --git a/cli/src/main/java/com/github/exadmin/cyberferret/CyberFerretCLI.java b/cli/src/main/java/com/github/exadmin/cyberferret/CyberFerretCLI.java
index 35e9dd1..994879a 100644
--- a/cli/src/main/java/com/github/exadmin/cyberferret/CyberFerretCLI.java
+++ b/cli/src/main/java/com/github/exadmin/cyberferret/CyberFerretCLI.java
@@ -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);
diff --git a/common/src/main/java/com/github/exadmin/cyberferret/async/RunnableCheckOnlineDictionary.java b/common/src/main/java/com/github/exadmin/cyberferret/async/RunnableCheckOnlineDictionary.java
index d8224a7..37ebcd3 100644
--- a/common/src/main/java/com/github/exadmin/cyberferret/async/RunnableCheckOnlineDictionary.java
+++ b/common/src/main/java/com/github/exadmin/cyberferret/async/RunnableCheckOnlineDictionary.java
@@ -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();
diff --git a/common/src/main/java/com/github/exadmin/cyberferret/utils/GitUtils.java b/common/src/main/java/com/github/exadmin/cyberferret/utils/GitUtils.java
index 56de491..1122dc7 100644
--- a/common/src/main/java/com/github/exadmin/cyberferret/utils/GitUtils.java
+++ b/common/src/main/java/com/github/exadmin/cyberferret/utils/GitUtils.java
@@ -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;
@@ -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");
diff --git a/hooks/cyberferret.sh b/hooks/cyberferret.sh
new file mode 100755
index 0000000..0888267
--- /dev/null
+++ b/hooks/cyberferret.sh
@@ -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"
diff --git a/pom.xml b/pom.xml
index bd197f9..48013f7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
cyber-ferret
- 1.2.3
+ 1.2.4
UTF-8
5.10.2
21