diff --git a/README.md b/README.md index 972ed5f..85ebe40 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,23 @@ jobs: | `run-every-commit` | Run ClaudeCode on every commit (skips cache check). Warning: May increase false positives on PRs with many commits. | `false` | No | | `false-positive-filtering-instructions` | Path to custom false positive filtering instructions text file | None | No | | `custom-security-scan-instructions` | Path to custom security scan instructions text file to append to audit prompt | None | No | +| `allowed-tools` | Comma-separated list of tools Claude Code can use (e.g., `"Read,Grep,LS,Bash(git diff:*)"`). If not specified, uses default read-only tools. | See below | No | + +#### Default Allowed Tools + +When `allowed-tools` is not specified, the action uses these read-only tools by default: +- `Read` - Read file contents +- `Glob` - Search for files by pattern +- `Grep` - Search file contents +- `LS` - List directory contents +- `Task` - Create subtasks for analysis +- `Bash(git diff:*)` - View git diffs +- `Bash(git status:*)` - Check git status +- `Bash(git log:*)` - View git history +- `Bash(git show:*)` - Show git objects +- `Bash(git remote show:*)` - Show remote information + +These defaults ensure Claude Code can analyze code without making any modifications. ### Action Outputs diff --git a/action.yml b/action.yml index 3c8f3ff..3f85457 100644 --- a/action.yml +++ b/action.yml @@ -48,6 +48,11 @@ inputs: required: false default: '' + allowed-tools: + description: 'Comma-separated list of tools Claude Code is allowed to use (e.g., "Read,Grep,LS,Bash(git diff:*),Bash(git status:*)"). If not specified, uses default read-only tools for security scanning.' + required: false + default: '' + outputs: findings-count: description: 'Number of security findings' @@ -175,6 +180,7 @@ runs: FALSE_POSITIVE_FILTERING_INSTRUCTIONS: ${{ inputs.false-positive-filtering-instructions }} CUSTOM_SECURITY_SCAN_INSTRUCTIONS: ${{ inputs.custom-security-scan-instructions }} CLAUDE_MODEL: ${{ inputs.claude-model }} + ALLOWED_TOOLS: ${{ inputs.allowed-tools }} run: | echo "Running ClaudeCode AI security analysis..." echo "----------------------------------------" diff --git a/claudecode/github_action_audit.py b/claudecode/github_action_audit.py index 89fe82d..1d32544 100644 --- a/claudecode/github_action_audit.py +++ b/claudecode/github_action_audit.py @@ -227,6 +227,15 @@ def run_security_audit(self, repo_dir: Path, prompt: str) -> Tuple[bool, str, Di '--model', DEFAULT_CLAUDE_MODEL ] + # Add allowed tools if specified + allowed_tools = os.environ.get('ALLOWED_TOOLS', '').strip() + if not allowed_tools: + # Default read-only tools for security scanning + allowed_tools = 'Read,Glob,Grep,LS,Task,Bash(git diff:*),Bash(git status:*),Bash(git log:*),Bash(git show:*),Bash(git remote show:*)' + + # Claude CLI handles comma-separated list directly + cmd.extend(['--allowedTools', allowed_tools]) + # Run Claude Code with retry logic NUM_RETRIES = 3 for attempt in range(NUM_RETRIES): diff --git a/claudecode/test_claude_runner.py b/claudecode/test_claude_runner.py index d2032d8..1477ca4 100644 --- a/claudecode/test_claude_runner.py +++ b/claudecode/test_claude_runner.py @@ -166,7 +166,9 @@ def test_run_security_audit_success(self, mock_run): assert call_args[0][0] == [ 'claude', '--output-format', 'json', - '--model', DEFAULT_CLAUDE_MODEL + '--model', DEFAULT_CLAUDE_MODEL, + '--allowedTools', + 'Read,Glob,Grep,LS,Task,Bash(git diff:*),Bash(git status:*),Bash(git log:*),Bash(git show:*),Bash(git remote show:*)' ] assert call_args[1]['input'] == 'test prompt' assert call_args[1]['cwd'] == Path('/tmp/test') diff --git a/claudecode/test_github_action_audit.py b/claudecode/test_github_action_audit.py index cd22805..8f9a3a1 100644 --- a/claudecode/test_github_action_audit.py +++ b/claudecode/test_github_action_audit.py @@ -210,6 +210,59 @@ def test_get_security_audit_prompt(self): assert 'test.py' in prompt +class TestPermissionRestrictions: + """Test permission restrictions for Claude Code CLI.""" + + def test_default_allowed_tools(self): + """Test that default allowed tools are set when ALLOWED_TOOLS is not specified.""" + import os + from unittest.mock import patch, MagicMock + from claudecode.github_action_audit import SimpleClaudeRunner + from pathlib import Path + + with patch.dict(os.environ, {}, clear=True): + runner = SimpleClaudeRunner() + + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stdout = '{"result": "{\\"findings\\": []}"}' + + with patch('subprocess.run', return_value=mock_result) as mock_run: + runner.run_security_audit(Path('.'), 'test prompt') + cmd = mock_run.call_args[0][0] + + # Verify --allowedTools flag is present with default tools + assert '--allowedTools' in cmd + idx = cmd.index('--allowedTools') + tools_string = cmd[idx + 1] + assert 'Read' in tools_string + assert 'Bash(git diff:*)' in tools_string + + def test_custom_allowed_tools(self): + """Test that custom allowed tools override defaults.""" + import os + from unittest.mock import patch, MagicMock + from claudecode.github_action_audit import SimpleClaudeRunner + from pathlib import Path + + with patch.dict(os.environ, {'ALLOWED_TOOLS': 'Read,Grep'}): + runner = SimpleClaudeRunner() + + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stdout = '{"result": "{\\"findings\\": []}"}' + + with patch('subprocess.run', return_value=mock_result) as mock_run: + runner.run_security_audit(Path('.'), 'test prompt') + cmd = mock_run.call_args[0][0] + + # Verify only custom tools are present + idx = cmd.index('--allowedTools') + tools_string = cmd[idx + 1] + assert tools_string == 'Read,Grep' + assert 'Bash(git diff:*)' not in tools_string # Default tool not included + + class TestDeploymentPRDetection: """Test deployment PR title pattern matching."""