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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 "----------------------------------------"
Expand Down
9 changes: 9 additions & 0 deletions claudecode/github_action_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 3 additions & 1 deletion claudecode/test_claude_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
53 changes: 53 additions & 0 deletions claudecode/test_github_action_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down