Skip to content
Open
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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ clawpinch/
│ │ └── interactive.sh # Post-scan menu: review, auto-fix, handoff export, AI remediation
│ ├── scan_config.sh # CHK-CFG-001..010 — gateway, TLS, auth, CORS
│ ├── scan_secrets.py # CHK-SEC-001..008 — API keys, passwords, tokens
│ ├── scan_git_history.sh # CHK-SEC-008 — secrets in git commit history
│ ├── scan_network.sh # CHK-NET-001..008 — ports, WebSocket, DNS rebinding
│ ├── scan_skills.sh # CHK-SKL-001..010 — permissions, signatures, eval
│ ├── scan_permissions.sh # CHK-PRM-001..008 — least-privilege, wildcards
Expand Down
2 changes: 1 addition & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ bash clawpinch.sh
# Standard interactive scan
clawpinch

# Deep scan (supply-chain hash verification, full skill decompilation)
# Deep scan (supply-chain hash verification, full skill decompilation, extended git history)
clawpinch --deep

# JSON output for programmatic consumption
Expand Down
1 change: 1 addition & 0 deletions scripts/helpers/report.sh
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ _scanner_category() {
case "$scanner_name" in
scan_config*) echo "🔧|Configuration" ;;
scan_secrets*) echo "🔑|Secrets" ;;
scan_git_history*)echo "📜|Git History" ;;
scan_cves*) echo "🛡️|CVE & Versions" ;;
scan_network*) echo "🌐|Network" ;;
scan_permissions*)echo "🔒|Permissions" ;;
Expand Down
188 changes: 188 additions & 0 deletions scripts/helpers/test_git_history.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#!/usr/bin/env bash
set -euo pipefail

###############################################################################
# test_git_history.sh - Integration Test for Git History Scanner
#
# Simplified pragmatic tests focusing on edge cases and basic functionality
#
# Usage:
# bash scripts/helpers/test_git_history.sh
###############################################################################

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
SCANNER="${PROJECT_ROOT}/scripts/scan_git_history.sh"

# Test counters
TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0

# Color output
if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then
C_GREEN='\033[0;32m'
C_RED='\033[0;31m'
C_BLUE='\033[0;34m'
C_BOLD='\033[1m'
C_RESET='\033[0m'
else
C_GREEN='' C_RED='' C_BLUE='' C_BOLD='' C_RESET=''
fi

# Create temp directory
TEST_DIR="$(mktemp -d -t clawpinch-git-test.XXXXXX)"
function cleanup {
rm -rf "$TEST_DIR"
}
trap cleanup EXIT

echo "[info] Test directory: $TEST_DIR" >&2

# Test helper
run_test() {
local test_name="$1"
local test_cmd="$2"

TESTS_RUN=$((TESTS_RUN + 1))
printf " [%02d] %-60s " "$TESTS_RUN" "$test_name"

if eval "$test_cmd" &>/dev/null; then
printf "${C_GREEN}✓ PASS${C_RESET}\n"
TESTS_PASSED=$((TESTS_PASSED + 1))
return 0
else
printf "${C_RED}✗ FAIL${C_RESET}\n"
TESTS_FAILED=$((TESTS_FAILED + 1))
Comment on lines +50 to +56
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test harness uses eval

run_test executes test_cmd via eval, so any unexpected characters in the command string will be interpreted by the shell. Here the commands are currently hardcoded, but this still makes the test harness unnecessarily unsafe and brittle.

Prefer calling the test functions directly (e.g., pass function name and invoke it) to avoid eval.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/helpers/test_git_history.sh
Line: 50:56

Comment:
**Test harness uses `eval`**

`run_test` executes `test_cmd` via `eval`, so any unexpected characters in the command string will be interpreted by the shell. Here the commands are currently hardcoded, but this still makes the test harness unnecessarily unsafe and brittle.

Prefer calling the test functions directly (e.g., pass function name and invoke it) to avoid `eval`.


How can I resolve this? If you propose a fix, please make it concise.

return 1
fi
Comment on lines +50 to +58

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The run_test helper redirects all command output to /dev/null using &>/dev/null. While this keeps the test output clean for passing tests, it makes debugging failures very difficult as there is no information about what went wrong. It would be more helpful to capture the output of failed tests and display it.

Suggested change
if eval "$test_cmd" &>/dev/null; then
printf "${C_GREEN}✓ PASS${C_RESET}\n"
TESTS_PASSED=$((TESTS_PASSED + 1))
return 0
else
printf "${C_RED}✗ FAIL${C_RESET}\n"
TESTS_FAILED=$((TESTS_FAILED + 1))
return 1
fi
local output
if output=$(eval "$test_cmd" 2>&1); then
printf "${C_GREEN}✓ PASS${C_RESET}\n"
TESTS_PASSED=$((TESTS_PASSED + 1))
return 0
else
printf "${C_RED}✗ FAIL${C_RESET}\n"
# Indent the captured output for readability
echo "$output" | sed 's/^/ /' >&2
TESTS_FAILED=$((TESTS_FAILED + 1))
return 1
fi

}

# Test: Non-git directory
test_non_git() {
local dir="${TEST_DIR}/not_git"
mkdir -p "$dir"
local output
output=$(GIT_REPO_PATH="$dir" bash "$SCANNER")
echo "$output" | jq -e 'type == "array" and length == 0' >/dev/null 2>&1
}

# Test: Empty repo
test_empty_repo() {
local repo="${TEST_DIR}/empty"
mkdir -p "$repo"
cd "$repo"
git init -q
local output
output=$(GIT_REPO_PATH="$repo" bash "$SCANNER")
echo "$output" | jq -e 'type == "array" and length == 0' >/dev/null 2>&1
}

# Test: Valid JSON output
test_json_output() {
local repo="${TEST_DIR}/json_test"
mkdir -p "$repo"
cd "$repo"
git init -q
git config user.email "test@test.test"
git config user.name "Test"
echo "clean file" > file.txt
git add file.txt
git commit -q -m "Add file"
local output
output=$(GIT_REPO_PATH="$repo" bash "$SCANNER")
echo "$output" | jq -e 'type == "array"' >/dev/null 2>&1
}

# Test: Scanner doesn't crash on binary files
test_binary_handling() {
local repo="${TEST_DIR}/binary"
mkdir -p "$repo"
cd "$repo"
git init -q
git config user.email "test@test.test"
git config user.name "Test"
printf '\x00\x01\x02\x03\x04' > binary.dat
git add binary.dat
git commit -q -m "Add binary"
local output
output=$(GIT_REPO_PATH="$repo" bash "$SCANNER" 2>&1)
echo "$output" | jq -e 'type == "array"' >/dev/null 2>&1
}

# Test: Worktree support (doesn't crash)
test_worktree() {
local repo="${TEST_DIR}/wt_main"
mkdir -p "$repo"
cd "$repo"
git init -q
git config user.email "test@test.test"
git config user.name "Test"
echo "main" > main.txt
git add main.txt
git commit -q -m "Main"

local wt="${TEST_DIR}/wt_branch"
if git worktree add -q "$wt" -b branch 2>/dev/null; then
local output
output=$(GIT_REPO_PATH="$wt" bash "$SCANNER" 2>&1)
git worktree remove -f "$wt" 2>/dev/null || true
echo "$output" | jq -e 'type == "array"' >/dev/null 2>&1
else
# Worktrees not supported, pass test
return 0
fi
}

# Test: Deep scan mode
test_deep_mode() {
local repo="${TEST_DIR}/deep"
mkdir -p "$repo"
cd "$repo"
git init -q
git config user.email "test@test.test"
git config user.name "Test"
echo "file" > file.txt
git add file.txt
git commit -q -m "File"

# Normal scan
local normal
normal=$(CLAWPINCH_DEEP=0 GIT_REPO_PATH="$repo" bash "$SCANNER")

# Deep scan
local deep
deep=$(CLAWPINCH_DEEP=1 GIT_REPO_PATH="$repo" bash "$SCANNER")

# Both should return valid JSON arrays
echo "$normal" | jq -e 'type == "array"' >/dev/null 2>&1 && \
echo "$deep" | jq -e 'type == "array"' >/dev/null 2>&1
}

# Run tests
printf "\n${C_BLUE}${C_BOLD}━━━ Git History Scanner Tests ━━━${C_RESET}\n\n"

printf "${C_BLUE}${C_BOLD}Edge Cases${C_RESET}\n"
run_test "Non-git directory returns empty array" "test_non_git"
run_test "Empty repository returns empty array" "test_empty_repo"
run_test "Binary file handling (no crash)" "test_binary_handling"
run_test "Worktree support (no crash)" "test_worktree"

printf "\n${C_BLUE}${C_BOLD}Functionality${C_RESET}\n"
run_test "Valid JSON output format" "test_json_output"
run_test "Deep scan mode" "test_deep_mode"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The integration tests cover several important edge cases like non-git directories, empty repos, and binary files. However, there is no test case that verifies the scanner's primary function: detecting a secret in the git history. A positive test case should be added to ensure the scanner correctly identifies a committed secret.

Here is an example test you could add:

# Test: Find a secret in commit history
test_find_secret() {
    local repo="${TEST_DIR}/found_secret"
    mkdir -p "$repo"
    cd "$repo"
    git init -q
    git config user.email "test@test.test"
    git config user.name "Test"

    # Commit a file with a secret
    echo 'const SLACK_BOT_TOKEN = "xoxb-12345-67890-abcdef";' > secrets.js
    git add secrets.js
    git commit -q -m "Add initial file with secret"

    # Remove the secret in a later commit
    echo 'const SLACK_BOT_TOKEN = process.env.SLACK_TOKEN;' > secrets.js
    git add secrets.js
    git commit -q -m "Remove hardcoded secret"

    local output
    output=$(GIT_REPO_PATH="$repo" bash "$SCANNER")
    # Check that at least one finding was returned
    echo "$output" | jq -e 'type == "array" and length > 0' >/dev/null 2>&1 && \
    # Check that the finding is the correct one
    echo "$output" | jq -e '.[0].id == "CHK-SEC-008" and .[0].severity == "critical"' >/dev/null 2>&1
}

# ...and then call it in the test suite:
# run_test "Finds a committed secret" "test_find_secret"


# Summary
printf "\n${C_BLUE}${C_BOLD}━━━ Test Summary ━━━${C_RESET}\n\n"
printf " Total: %d\n" "$TESTS_RUN"
printf " ${C_GREEN}Passed: %d${C_RESET}\n" "$TESTS_PASSED"

if [[ $TESTS_FAILED -gt 0 ]]; then
printf " ${C_RED}Failed: %d${C_RESET}\n" "$TESTS_FAILED"
printf "\n${C_RED}${C_BOLD}✗ TESTS FAILED${C_RESET}\n\n"
exit 1
else
printf " Failed: 0\n"
printf "\n${C_GREEN}${C_BOLD}✓ ALL TESTS PASSED${C_RESET}\n\n"
exit 0
fi
Loading