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