Skip to content
Closed
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
113 changes: 113 additions & 0 deletions tests/content-validation.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,116 @@
import { describe, expect, it } from "vitest";
import path from "node:path";
import fs from "node:fs";

const repoRoot = path.resolve(import.meta.dirname, "..");
const fixturesRoot = path.join(repoRoot, "tests/fixtures/hooks");

describe("hook content validation", () => {
describe("security regression fixtures for hook script bodies", () => {
// Vulnerable fixtures tests
it("has vulnerable fixture for predictable /tmp/ debug logs", () => {
const fixturePath = path.join(fixturesRoot, "vulnerable-tmp-debug-log.mdx");
expect(fs.existsSync(fixturePath)).toBe(true);

const content = fs.readFileSync(fixturePath, "utf8");
expect(content).toContain('scriptBody:');
expect(content).toContain('/tmp/');
expect(content).not.toContain('mktemp');
});

it("has vulnerable fixture for community download references", () => {
const fixturePath = path.join(fixturesRoot, "vulnerable-community-download.mdx");
expect(fs.existsSync(fixturePath)).toBe(true);

const content = fs.readFileSync(fixturePath, "utf8");
expect(content).toContain('downloadUrl:');
expect(content).toContain('.zip');
expect(content).toContain('curl');
});

it("has vulnerable fixture for missing safety notes", () => {
const fixturePath = path.join(fixturesRoot, "vulnerable-missing-safety-notes.mdx");
expect(fs.existsSync(fixturePath)).toBe(true);

const content = fs.readFileSync(fixturePath, "utf8");
expect(content).toContain('scriptBody:');
expect(content).toContain('delete');
expect(content).not.toContain('safetyNotes:');
});

it("has vulnerable fixture for missing privacy notes", () => {
const fixturePath = path.join(fixturesRoot, "vulnerable-missing-privacy-notes.mdx");
expect(fs.existsSync(fixturePath)).toBe(true);

const content = fs.readFileSync(fixturePath, "utf8");
expect(content).toContain('scriptBody:');
expect(content).toContain('find');
expect(content).toContain('curl');
expect(content).not.toContain('privacyNotes:');
});

// Safe fixtures tests
it("has safe fixture with proper quoting", () => {
const fixturePath = path.join(fixturesRoot, "safe-proper-quoting.mdx");
expect(fs.existsSync(fixturePath)).toBe(true);

const content = fs.readFileSync(fixturePath, "utf8");
expect(content).toContain('scriptBody:');
expect(content).toContain('"$');
});

it("has safe fixture with secure tmp usage", () => {
const fixturePath = path.join(fixturesRoot, "safe-secure-tmp-usage.mdx");
expect(fs.existsSync(fixturePath)).toBe(true);

const content = fs.readFileSync(fixturePath, "utf8");
expect(content).toContain('scriptBody:');
expect(content).toContain('mktemp');
expect(content).toContain('trap');
});

it("has safe fixture without external downloads", () => {
const fixturePath = path.join(fixturesRoot, "safe-no-external-downloads.mdx");
expect(fs.existsSync(fixturePath)).toBe(true);

const content = fs.readFileSync(fixturePath, "utf8");
expect(content).toContain('scriptBody:');
expect(content).toContain('repoUrl:');
expect(content).not.toContain('downloadUrl:');
});

it("has safe fixture with safety notes for destructive actions", () => {
const fixturePath = path.join(fixturesRoot, "safe-with-safety-notes.mdx");
expect(fs.existsSync(fixturePath)).toBe(true);

const content = fs.readFileSync(fixturePath, "utf8");
expect(content).toContain('scriptBody:');
expect(content).toContain('safetyNotes:');
expect(content).toContain('delete');
expect(content).toContain('Deletes temporary files');
});

it("has safe fixture with privacy notes for local data access", () => {
const fixturePath = path.join(fixturesRoot, "safe-with-privacy-notes.mdx");
expect(fs.existsSync(fixturePath)).toBe(true);

const content = fs.readFileSync(fixturePath, "utf8");
expect(content).toContain('scriptBody:');
expect(content).toContain('privacyNotes:');
expect(content).toContain('find');
expect(content).toContain('Reads local workspace');
});

// Summary test
it("has all 10 security regression fixtures (5 vulnerable + 5 safe)", () => {
const fixtures = fs.readdirSync(fixturesRoot);
const vulnerableFixtures = fixtures.filter(f => f.startsWith('vulnerable-'));
const safeFixtures = fixtures.filter(f => f.startsWith('safe-'));

expect(vulnerableFixtures).toHaveLength(5);
expect(safeFixtures).toHaveLength(5);
expect(fixtures.length).toBeGreaterThanOrEqual(10);
});
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
Expand Down
26 changes: 26 additions & 0 deletions tests/fixtures/hooks/safe-no-external-downloads.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
title: Safe Hook - No External Downloads
slug: safe-no-external-downloads
category: hooks
description: Hook without external download references
cardDescription: Demonstrates safe hook without downloads
trigger: PostToolUse
repoUrl: https://github.com/example/safe-hook
scriptLanguage: bash
scriptBody: |
#!/usr/bin/env bash
# This hook operates without downloading external archives

# Check if required tools are available
if ! command -v git &> /dev/null; then
echo "Git is required but not installed"
exit 1
fi

# Perform local operations only
git status --short

echo "Hook completed successfully"
---

This hook demonstrates safe operation without external download/archive references.
24 changes: 24 additions & 0 deletions tests/fixtures/hooks/safe-proper-quoting.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
title: Safe Hook - Proper Shell Quoting
slug: safe-proper-quoting
category: hooks
description: Hook with proper shell quoting in embedded scripts
cardDescription: Demonstrates safe quoting practices
trigger: PostToolUse
scriptLanguage: bash
scriptBody: |
#!/usr/bin/env bash
# This hook demonstrates proper shell quoting practices

# Properly quoted variable expansion
FILE_PATH="$1"

# Safe python -c usage with proper quoting
python3 -c 'import sys; print("Processing:", sys.argv[1])' "$FILE_PATH"

# Properly quoted command substitution
TIMESTAMP="$(date +%Y-%m-%d)"
echo "Processed at: $TIMESTAMP"
---

This hook demonstrates safe shell quoting practices in embedded scripts.
28 changes: 28 additions & 0 deletions tests/fixtures/hooks/safe-secure-tmp-usage.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
title: Safe Hook - Secure Tmp Usage
slug: safe-secure-tmp-usage
category: hooks
description: Hook with secure temporary file handling using mktemp
cardDescription: Demonstrates secure tmp file practices
trigger: PostToolUse
scriptLanguage: bash
scriptBody: |
#!/usr/bin/env bash
# This hook demonstrates secure temporary file handling

# Use mktemp for secure temporary file creation
DEBUG_LOG=$(mktemp /tmp/claude-hook.XXXXXX)

# Ensure cleanup on exit
trap 'rm -f "$DEBUG_LOG"' EXIT

echo "Processing hook at $(date)" >> "$DEBUG_LOG"
echo "Current directory: $(pwd)" >> "$DEBUG_LOG"

# Perform operation with secure temp file
echo "Hook completed" >> "$DEBUG_LOG"

# File is automatically cleaned up by trap
---

This hook demonstrates secure temporary file handling using mktemp with proper cleanup.
31 changes: 31 additions & 0 deletions tests/fixtures/hooks/safe-with-privacy-notes.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
title: Safe Hook - With Privacy Notes
slug: safe-with-privacy-notes
category: hooks
description: Hook with local data access and proper privacy notes disclosure
cardDescription: Demonstrates proper privacy notes disclosure
trigger: SessionStart
scriptLanguage: bash
privacyNotes:
- "Reads local workspace path and project name for session tracking"
- "Collects file count statistics from current directory"
- "Does not send any data to external services or third parties"
- "All collected data remains local to the user's machine"
scriptBody: |
#!/usr/bin/env bash
# This hook accesses local workspace data with privacy disclosure

# Read workspace metadata (with privacy notes disclosure)
WORKSPACE_PATH=$(pwd)
PROJECT_NAME=$(basename "$WORKSPACE_PATH")

# Collect file statistics (with privacy notes disclosure)
FILE_COUNT=$(find . -type f 2>/dev/null | wc -l)

# Store locally only (no external transmission)
echo "Session started: $PROJECT_NAME ($FILE_COUNT files)" >> ~/.claude/session.log

echo "Session tracking initialized (local only)"
---

This hook demonstrates proper privacy notes disclosure for local data access.
28 changes: 28 additions & 0 deletions tests/fixtures/hooks/safe-with-safety-notes.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
title: Safe Hook - With Safety Notes
slug: safe-with-safety-notes
category: hooks
description: Hook with destructive actions and proper safety notes disclosure
cardDescription: Demonstrates proper safety notes disclosure
trigger: Stop
scriptLanguage: bash
safetyNotes:
- "Deletes temporary files matching claude-* pattern in /tmp directory"
- "Removes log files older than 7 days from ~/.claude/logs directory"
- "Only operates on files owned by the current user"
scriptBody: |
#!/usr/bin/env bash
# This hook performs destructive actions with proper safety disclosure

# Clean up temporary files (with safety notes disclosure)
find /tmp -name "claude-*" -type f -user "$(whoami)" -delete 2>/dev/null

# Remove old log files (with safety notes disclosure)
if [ -d ~/.claude/logs ]; then
find ~/.claude/logs -name "*.log" -mtime +7 -user "$(whoami)" -delete 2>/dev/null
fi

echo "Cleanup completed safely"
---

This hook demonstrates proper safety notes disclosure for destructive actions.
20 changes: 20 additions & 0 deletions tests/fixtures/hooks/vulnerable-community-download.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: Vulnerable Hook - Community Download Reference
slug: vulnerable-community-download
category: hooks
description: Hook with community download/archive hosting references when not allowed
cardDescription: Demonstrates community download vulnerability
trigger: PostToolUse
downloadUrl: https://example.com/downloads/hook-package.zip
scriptLanguage: bash
scriptBody: |
#!/usr/bin/env bash
# This hook references a community-hosted download archive
# which is not allowed without maintainer review

curl -L https://example.com/downloads/hook-package.zip -o /tmp/hook.zip
unzip /tmp/hook.zip -d ~/.claude/hooks/
chmod +x ~/.claude/hooks/hook-script.sh
---

This hook demonstrates a vulnerability where community download/archive hosting is referenced without proper review.
11 changes: 11 additions & 0 deletions tests/fixtures/hooks/vulnerable-invalid-quoting.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Vulnerable Hook - Invalid Shell Quoting
slug: vulnerable-invalid-quoting
category: hooks
description: Hook with invalid shell quoting in embedded python -c script
cardDescription: Demonstrates invalid quoting vulnerability
trigger: PostToolUse
scriptLanguage: bash
---

This hook demonstrates invalid shell quoting inside embedded `python -c` scripts.
28 changes: 28 additions & 0 deletions tests/fixtures/hooks/vulnerable-missing-privacy-notes.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
title: Vulnerable Hook - Missing Privacy Notes
slug: vulnerable-missing-privacy-notes
category: hooks
description: Hook with local data access but missing required privacy notes
cardDescription: Demonstrates missing privacy notes vulnerability
trigger: SessionStart
scriptLanguage: bash
scriptBody: |
#!/usr/bin/env bash
# This hook accesses local workspace data without privacy disclosure

# Read workspace metadata (local data access)
WORKSPACE_PATH=$(pwd)
PROJECT_NAME=$(basename "$WORKSPACE_PATH")

# Collect file statistics (local data access)
FILE_COUNT=$(find . -type f | wc -l)

# Send to analytics endpoint (requires privacy notes)
curl -X POST https://analytics.example.com/track \
-H "Content-Type: application/json" \
-d "{\"project\":\"$PROJECT_NAME\",\"files\":$FILE_COUNT}"

echo "Session tracking initialized"
---

This hook demonstrates a vulnerability where local data access occurs without required privacy notes disclosure.
22 changes: 22 additions & 0 deletions tests/fixtures/hooks/vulnerable-missing-safety-notes.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: Vulnerable Hook - Missing Safety Notes
slug: vulnerable-missing-safety-notes
category: hooks
description: Hook with destructive actions but missing required safety notes
cardDescription: Demonstrates missing safety notes vulnerability
trigger: Stop
scriptLanguage: bash
scriptBody: |
#!/usr/bin/env bash
# This hook performs destructive actions without proper safety disclosure

# Clean up temporary files (destructive action)
find /tmp -name "claude-*" -type f -delete

# Remove old log files (destructive action)
find ~/.claude/logs -name "*.log" -mtime +7 -delete

echo "Cleanup completed"
---

This hook demonstrates a vulnerability where destructive actions are performed without required safety notes disclosure.
25 changes: 25 additions & 0 deletions tests/fixtures/hooks/vulnerable-tmp-debug-log.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: Vulnerable Hook - Predictable Tmp Debug Log
slug: vulnerable-tmp-debug-log
category: hooks
description: Hook with predictable /tmp/ debug logs that could leak sensitive output
cardDescription: Demonstrates predictable tmp log vulnerability
trigger: PostToolUse
scriptLanguage: bash
scriptBody: |
#!/usr/bin/env bash
# This hook writes debug output to a predictable /tmp/ location
# which could leak sensitive hook output to other users

DEBUG_LOG="/tmp/claude-hook-debug.log"

echo "Processing hook at $(date)" >> "$DEBUG_LOG"
echo "Current directory: $(pwd)" >> "$DEBUG_LOG"
echo "Environment variables:" >> "$DEBUG_LOG"
env >> "$DEBUG_LOG"

# Perform some operation
echo "Hook completed" >> "$DEBUG_LOG"
---

This hook demonstrates a vulnerability where debug logs are written to a predictable shared `/tmp/` location.
Loading
Loading