Implement minimal-permission security model for auto-tagging operations using dedicated entrypoint scripts and CLI-based labeling to improve reliability and reduce attack surface

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jonathan Flatt
2025-05-25 22:58:20 -05:00
parent 582c785a67
commit 8e2e30e38b
5 changed files with 239 additions and 99 deletions

View File

@@ -85,13 +85,26 @@ Use the demo repository for testing auto-tagging and webhook functionality:
## Features
### Auto-Tagging
The system automatically analyzes new issues and applies appropriate labels based on:
The system automatically analyzes new issues and applies appropriate labels using a secure, minimal-permission approach:
**Security Features:**
- **Minimal Tool Access**: Uses only `Read` and `GitHub` tools (no file editing or bash execution)
- **Dedicated Container**: Runs in specialized container with restricted entrypoint script
- **CLI-Based**: Uses `gh` CLI commands directly instead of JSON parsing for better reliability
**Label Categories:**
- **Priority**: critical, high, medium, low
- **Type**: bug, feature, enhancement, documentation, question, security
- **Complexity**: trivial, simple, moderate, complex
- **Component**: api, frontend, backend, database, auth, webhook, docker
When an issue is opened, Claude analyzes the title and description to suggest intelligent labels, with keyword-based fallback for reliability.
**Process Flow:**
1. New issue triggers `issues.opened` webhook
2. Dedicated Claude container starts with `claudecode-tagging-entrypoint.sh`
3. Claude analyzes issue content using minimal tools
4. Labels applied directly via `gh issue edit --add-label` commands
5. No comments posted (silent operation)
6. Fallback to keyword-based labeling if CLI approach fails
### Automated PR Review
The system automatically triggers comprehensive PR reviews when all checks pass:
@@ -119,20 +132,32 @@ The system automatically triggers comprehensive PR reviews when all checks pass:
- `awsCredentialProvider.js` - Secure AWS credential management
- `sanitize.js` - Input sanitization and security
### Execution Modes
- **Direct mode**: Runs Claude Code CLI locally
- **Container mode**: Runs Claude in isolated Docker containers with elevated privileges
### Execution Modes & Security Architecture
The system uses different execution modes based on operation type:
### DevContainer Configuration
The repository includes a `.devcontainer` configuration that allows Claude Code to run with:
**Operation Types:**
- **Auto-tagging**: Minimal permissions (`Read`, `GitHub` tools only)
- **PR Review**: Standard permissions (full tool set)
- **Default**: Standard permissions (full tool set)
**Security Features:**
- **Tool Allowlists**: Each operation type uses specific tool restrictions
- **Dedicated Entrypoints**: Separate container entrypoint scripts for different operations
- **No Dangerous Permissions**: System avoids `--dangerously-skip-permissions` flag
- **Container Isolation**: Docker containers with minimal required capabilities
**Container Entrypoints:**
- `claudecode-tagging-entrypoint.sh`: Minimal tools for auto-tagging (`--allowedTools Read,GitHub`)
- `claudecode-entrypoint.sh`: Full tools for general operations (`--allowedTools Bash,Create,Edit,Read,Write,GitHub`)
**DevContainer Configuration:**
The repository includes a `.devcontainer` configuration for development:
- Privileged mode for system-level access
- Network capabilities (NET_ADMIN, NET_RAW) for firewall management
- System capabilities (SYS_TIME, DAC_OVERRIDE, AUDIT_WRITE, SYS_ADMIN)
- Docker socket mounting for container management
- Automatic firewall initialization via post-create command
This configuration enables the use of `--dangerously-skip-permissions` flag when running Claude Code CLI.
### Workflow
1. GitHub comment with bot mention (configured via BOT_USERNAME) triggers a webhook event
2. Express server receives the webhook at `/api/webhooks/github`

View File

@@ -0,0 +1,79 @@
#!/bin/bash
set -e
# Minimal entrypoint for auto-tagging workflow
# Only allows Read and GitHub tools for security
# Environment variables (passed from service)
# Simply reference the variables directly - no need to reassign
# They are already available in the environment
# Ensure workspace directory exists and has proper permissions
mkdir -p /workspace
chown -R node:node /workspace
# Configure GitHub authentication
if [ -n "${GITHUB_TOKEN}" ]; then
export GH_TOKEN="${GITHUB_TOKEN}"
echo "${GITHUB_TOKEN}" | sudo -u node gh auth login --with-token
sudo -u node gh auth setup-git
else
echo "No GitHub token provided, skipping GitHub authentication"
fi
# Clone the repository as node user (needed for context)
if [ -n "${GITHUB_TOKEN}" ] && [ -n "${REPO_FULL_NAME}" ]; then
echo "Cloning repository ${REPO_FULL_NAME}..." >&2
sudo -u node git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/${REPO_FULL_NAME}.git" /workspace/repo >&2
cd /workspace/repo
else
echo "Skipping repository clone - missing GitHub token or repository name" >&2
cd /workspace
fi
# Checkout main branch (tagging doesn't need specific branches)
echo "Using main branch" >&2
sudo -u node git checkout main >&2 || sudo -u node git checkout master >&2
# Configure git for minimal operations
sudo -u node git config --global user.email "${BOT_EMAIL:-claude@example.com}"
sudo -u node git config --global user.name "${BOT_USERNAME:-ClaudeBot}"
# Configure Anthropic API key
export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}"
# Create response file with proper permissions
RESPONSE_FILE="/workspace/response.txt"
touch "${RESPONSE_FILE}"
chown node:node "${RESPONSE_FILE}"
# Run Claude Code with minimal tools for auto-tagging
echo "Running Claude Code for auto-tagging..." >&2
# Check if command exists
if [ -z "${COMMAND}" ]; then
echo "ERROR: No command provided. COMMAND environment variable is empty." | tee -a "${RESPONSE_FILE}" >&2
exit 1
fi
# Log the command length for debugging
echo "Command length: ${#COMMAND}" >&2
# Run Claude Code with minimal tool set: Read (for repository context) and GitHub (for label operations)
sudo -u node -E env \
HOME="/home/node" \
PATH="/usr/local/bin:/usr/local/share/npm-global/bin:$PATH" \
ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}" \
GH_TOKEN="${GITHUB_TOKEN}" \
/usr/local/share/npm-global/bin/claude \
--allowedTools Read,GitHub \
--print "${COMMAND}" \
> "${RESPONSE_FILE}" 2>&1
# Check for errors
if [ $? -ne 0 ]; then
echo "ERROR: Claude Code execution failed. See logs for details." | tee -a "${RESPONSE_FILE}" >&2
fi
# Output the response
cat "${RESPONSE_FILE}"

View File

@@ -127,73 +127,48 @@ async function handleWebhook(req, res) {
);
try {
// Process the issue with Claude for automatic tagging
const tagCommand = `Analyze this issue and apply appropriate labels:
// Process the issue with Claude for automatic tagging using CLI-based approach
const tagCommand = `Analyze this GitHub issue and apply appropriate labels using GitHub CLI commands.
Title: ${issue.title}
Description: ${issue.body || 'No description provided'}
Issue Details:
- Title: ${issue.title}
- Description: ${issue.body || 'No description provided'}
- Issue Number: ${issue.number}
Available labels:
- Priority: critical, high, medium, low
- Type: bug, feature, enhancement, documentation, question, security
- Complexity: trivial, simple, moderate, complex
- Component: api, frontend, backend, database, auth, webhook, docker
Instructions:
1. First run 'gh label list' to see what labels are available in this repository
2. Analyze the issue content to determine appropriate labels from these categories:
- Priority: critical, high, medium, low
- Type: bug, feature, enhancement, documentation, question, security
- Complexity: trivial, simple, moderate, complex
- Component: api, frontend, backend, database, auth, webhook, docker
3. Apply the labels using: gh issue edit ${issue.number} --add-label "label1,label2,label3"
4. Do NOT comment on the issue - only apply labels silently
Return ONLY a JSON object with the labels to apply:
{
"labels": ["priority:medium", "type:feature", "complexity:simple", "component:api"]
}`;
Complete the auto-tagging task using only GitHub CLI commands.`;
logger.info('Sending issue to Claude for auto-tagging analysis');
logger.info('Sending issue to Claude for CLI-based auto-tagging');
const claudeResponse = await claudeService.processCommand({
repoFullName: repo.full_name,
issueNumber: issue.number,
command: tagCommand,
isPullRequest: false,
branchName: null
branchName: null,
operationType: 'auto-tagging'
});
// Parse Claude's response and apply labels
try {
// Extract JSON from Claude's response (it might have additional text) using safer approach
const jsonStart = claudeResponse.indexOf('{');
const jsonEnd = claudeResponse.lastIndexOf('}');
if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
const jsonString = claudeResponse.substring(jsonStart, jsonEnd + 1);
const labelSuggestion = JSON.parse(jsonString);
if (labelSuggestion.labels && Array.isArray(labelSuggestion.labels)) {
// Apply the suggested labels
await githubService.addLabelsToIssue({
repoOwner: repo.owner.login,
repoName: repo.name,
issueNumber: issue.number,
labels: labelSuggestion.labels
});
// Auto-tagging completed - no comment needed for subtlety
const sanitizedLabels = sanitizeLabels(labelSuggestion.labels);
logger.info(
{
repo: repo.full_name,
issue: issue.number,
labelCount: sanitizedLabels.length
},
'Auto-tagging completed successfully'
);
}
}
} catch (parseError) {
// With CLI-based approach, Claude handles the labeling directly
// Check if the response indicates success or if we need fallback
if (claudeResponse.includes('error') || claudeResponse.includes('failed')) {
logger.warn(
{
err: parseError,
claudeResponseLength: claudeResponse.length,
claudeResponsePreview: '[RESPONSE_CONTENT_REDACTED]'
repo: repo.full_name,
issue: issue.number,
responsePreview: claudeResponse.substring(0, 200)
},
'Failed to parse Claude response for auto-tagging'
'Claude CLI tagging may have failed, attempting fallback'
);
// Fall back to basic tagging based on keywords
const fallbackLabels = await githubService.getFallbackLabels(issue.title, issue.body);
if (fallbackLabels.length > 0) {
@@ -203,7 +178,17 @@ Return ONLY a JSON object with the labels to apply:
issueNumber: issue.number,
labels: fallbackLabels
});
logger.info('Applied fallback labels successfully');
}
} else {
logger.info(
{
repo: repo.full_name,
issue: issue.number,
responseLength: claudeResponse.length
},
'Auto-tagging completed with CLI approach'
);
}
return res.status(200).json({

View File

@@ -30,6 +30,7 @@ if (!BOT_USERNAME) {
* @param {string} options.command - The command to process with Claude
* @param {boolean} [options.isPullRequest=false] - Whether this is a pull request
* @param {string} [options.branchName] - The branch name for pull requests
* @param {string} [options.operationType='default'] - Operation type: 'auto-tagging', 'pr-review', or 'default'
* @returns {Promise<string>} - Claude's response
*/
async function processCommand({
@@ -37,7 +38,8 @@ async function processCommand({
issueNumber,
command,
isPullRequest = false,
branchName = null
branchName = null,
operationType = 'default'
}) {
try {
logger.info(
@@ -91,12 +93,59 @@ For real functionality, please configure valid GitHub and Claude API tokens.`;
});
}
// Select appropriate entrypoint script based on operation type
let entrypointScript;
switch (operationType) {
case 'auto-tagging':
entrypointScript = '/scripts/runtime/claudecode-tagging-entrypoint.sh';
logger.info({ operationType }, 'Using minimal tools for auto-tagging operation');
break;
case 'pr-review':
case 'default':
default:
entrypointScript = '/scripts/runtime/claudecode-entrypoint.sh';
logger.info({ operationType }, 'Using full tool set for standard operation');
break;
}
// Create unique container name (sanitized to prevent command injection)
const sanitizedRepoName = repoFullName.replace(/[^a-zA-Z0-9\-_]/g, '-');
const containerName = `claude-${sanitizedRepoName}-${Date.now()}`;
// Create the full prompt with context and instructions
const fullPrompt = `You are Claude, an AI assistant responding to a GitHub ${isPullRequest ? 'pull request' : 'issue'} via the ${BOT_USERNAME} webhook.
// Create the full prompt with context and instructions based on operation type
let fullPrompt;
if (operationType === 'auto-tagging') {
fullPrompt = `You are Claude, an AI assistant analyzing a GitHub issue for automatic label assignment.
**Context:**
- Repository: ${repoFullName}
- Issue Number: #${issueNumber}
- Operation: Auto-tagging (Read-only + Label assignment)
**Available Tools:**
- Read: Access repository files and issue content
- GitHub: Use 'gh' CLI for label operations only
**Task:**
Analyze the issue and apply appropriate labels using GitHub CLI commands. Use these categories:
- Priority: critical, high, medium, low
- Type: bug, feature, enhancement, documentation, question, security
- Complexity: trivial, simple, moderate, complex
- Component: api, frontend, backend, database, auth, webhook, docker
**Process:**
1. First run 'gh label list' to see available labels
2. Analyze the issue content
3. Use 'gh issue edit #{issueNumber} --add-label "label1,label2,label3"' to apply labels
4. Do NOT comment on the issue - only apply labels
**User Request:**
${command}
Complete the auto-tagging task using only the minimal required tools.`;
} else {
fullPrompt = `You are Claude, an AI assistant responding to a GitHub ${isPullRequest ? 'pull request' : 'issue'} via the ${BOT_USERNAME} webhook.
**Context:**
- Repository: ${repoFullName}
@@ -132,6 +181,7 @@ For real functionality, please configure valid GitHub and Claude API tokens.`;
${command}
Please complete this task fully and autonomously.`;
}
// Prepare environment variables for the container
const envVars = {
@@ -139,6 +189,7 @@ Please complete this task fully and autonomously.`;
ISSUE_NUMBER: issueNumber || '',
IS_PULL_REQUEST: isPullRequest ? 'true' : 'false',
BRANCH_NAME: branchName || '',
OPERATION_TYPE: operationType,
COMMAND: fullPrompt,
GITHUB_TOKEN: githubToken,
ANTHROPIC_API_KEY: secureCredentials.get('ANTHROPIC_API_KEY')
@@ -220,8 +271,8 @@ Please complete this task fully and autonomously.`;
}
});
// Add the image name as the final argument
dockerArgs.push(dockerImageName);
// Add the image name and custom entrypoint
dockerArgs.push('--entrypoint', entrypointScript, dockerImageName);
// Create sanitized version for logging (remove sensitive values)
const sanitizedArgs = dockerArgs.map(arg => {

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="jest tests" tests="34" failures="0" errors="0" time="0.301">
<testsuite name="GitHub Controller - Check Suite Events" errors="0" failures="0" skipped="2" timestamp="2025-05-26T02:09:45" time="0.119" tests="9">
<testcase classname="GitHub Controller - Check Suite Events should trigger PR review when check suite succeeds with PRs and combined status passes" name="GitHub Controller - Check Suite Events should trigger PR review when check suite succeeds with PRs and combined status passes" time="0.003">
<testsuites name="jest tests" tests="34" failures="0" errors="0" time="0.703">
<testsuite name="GitHub Controller - Check Suite Events" errors="0" failures="0" skipped="2" timestamp="2025-05-26T03:05:19" time="0.382" tests="9">
<testcase classname="GitHub Controller - Check Suite Events should trigger PR review when check suite succeeds with PRs and combined status passes" name="GitHub Controller - Check Suite Events should trigger PR review when check suite succeeds with PRs and combined status passes" time="0.005">
</testcase>
<testcase classname="GitHub Controller - Check Suite Events should not trigger PR review when check suite fails" name="GitHub Controller - Check Suite Events should not trigger PR review when check suite fails" time="0">
<testcase classname="GitHub Controller - Check Suite Events should not trigger PR review when check suite fails" name="GitHub Controller - Check Suite Events should not trigger PR review when check suite fails" time="0.001">
</testcase>
<testcase classname="GitHub Controller - Check Suite Events should not trigger PR review when check suite succeeds but has no PRs" name="GitHub Controller - Check Suite Events should not trigger PR review when check suite succeeds but has no PRs" time="0">
<testcase classname="GitHub Controller - Check Suite Events should not trigger PR review when check suite succeeds but has no PRs" name="GitHub Controller - Check Suite Events should not trigger PR review when check suite succeeds but has no PRs" time="0.001">
</testcase>
<testcase classname="GitHub Controller - Check Suite Events should handle multiple PRs in check suite in parallel" name="GitHub Controller - Check Suite Events should handle multiple PRs in check suite in parallel" time="0.001">
</testcase>
@@ -22,60 +22,60 @@
<testcase classname="GitHub Controller - Check Suite Events should skip PR review when already reviewed at same commit" name="GitHub Controller - Check Suite Events should skip PR review when already reviewed at same commit" time="0.001">
</testcase>
</testsuite>
<testsuite name="GitHub Controller" errors="0" failures="0" skipped="0" timestamp="2025-05-26T02:09:45" time="0.038" tests="4">
<testcase classname="GitHub Controller should process a valid webhook with @TestBot mention" name="GitHub Controller should process a valid webhook with @TestBot mention" time="0.001">
<testsuite name="githubService" errors="0" failures="0" skipped="0" timestamp="2025-05-26T03:05:19" time="0.082" tests="10">
<testcase classname="githubService getFallbackLabels should identify bug labels correctly" name="githubService getFallbackLabels should identify bug labels correctly" time="0.001">
</testcase>
<testcase classname="GitHub Controller should reject a webhook with invalid signature" name="GitHub Controller should reject a webhook with invalid signature" time="0.013">
</testcase>
<testcase classname="GitHub Controller should ignore comments without @TestBot mention" name="GitHub Controller should ignore comments without @TestBot mention" time="0">
</testcase>
<testcase classname="GitHub Controller should handle errors from Claude service" name="GitHub Controller should handle errors from Claude service" time="0">
</testcase>
</testsuite>
<testsuite name="githubService" errors="0" failures="0" skipped="0" timestamp="2025-05-26T02:09:45" time="0.052" tests="10">
<testcase classname="githubService getFallbackLabels should identify bug labels correctly" name="githubService getFallbackLabels should identify bug labels correctly" time="0">
</testcase>
<testcase classname="githubService getFallbackLabels should identify feature labels correctly" name="githubService getFallbackLabels should identify feature labels correctly" time="0.001">
<testcase classname="githubService getFallbackLabels should identify feature labels correctly" name="githubService getFallbackLabels should identify feature labels correctly" time="0">
</testcase>
<testcase classname="githubService getFallbackLabels should identify enhancement labels correctly" name="githubService getFallbackLabels should identify enhancement labels correctly" time="0">
</testcase>
<testcase classname="githubService getFallbackLabels should identify question labels correctly" name="githubService getFallbackLabels should identify question labels correctly" time="0">
<testcase classname="githubService getFallbackLabels should identify question labels correctly" name="githubService getFallbackLabels should identify question labels correctly" time="0.001">
</testcase>
<testcase classname="githubService getFallbackLabels should identify documentation labels correctly" name="githubService getFallbackLabels should identify documentation labels correctly" time="0">
</testcase>
<testcase classname="githubService getFallbackLabels should default to medium priority when no specific priority keywords found" name="githubService getFallbackLabels should default to medium priority when no specific priority keywords found" time="0">
</testcase>
<testcase classname="githubService getFallbackLabels should handle empty descriptions gracefully" name="githubService getFallbackLabels should handle empty descriptions gracefully" time="0.001">
<testcase classname="githubService getFallbackLabels should handle empty descriptions gracefully" name="githubService getFallbackLabels should handle empty descriptions gracefully" time="0">
</testcase>
<testcase classname="githubService addLabelsToIssue - test mode should return mock data in test mode" name="githubService addLabelsToIssue - test mode should return mock data in test mode" time="0">
<testcase classname="githubService addLabelsToIssue - test mode should return mock data in test mode" name="githubService addLabelsToIssue - test mode should return mock data in test mode" time="0.001">
</testcase>
<testcase classname="githubService createRepositoryLabels - test mode should return labels array in test mode" name="githubService createRepositoryLabels - test mode should return labels array in test mode" time="0.001">
<testcase classname="githubService createRepositoryLabels - test mode should return labels array in test mode" name="githubService createRepositoryLabels - test mode should return labels array in test mode" time="0">
</testcase>
<testcase classname="githubService postComment - test mode should return mock comment data in test mode" name="githubService postComment - test mode should return mock comment data in test mode" time="0">
<testcase classname="githubService postComment - test mode should return mock comment data in test mode" name="githubService postComment - test mode should return mock comment data in test mode" time="0.001">
</testcase>
</testsuite>
<testsuite name="Claude Service" errors="0" failures="0" skipped="0" timestamp="2025-05-26T02:09:45" time="0.027" tests="4">
<testsuite name="GitHub Controller" errors="0" failures="0" skipped="0" timestamp="2025-05-26T03:05:19" time="0.05" tests="4">
<testcase classname="GitHub Controller should process a valid webhook with @TestBot mention" name="GitHub Controller should process a valid webhook with @TestBot mention" time="0.002">
</testcase>
<testcase classname="GitHub Controller should reject a webhook with invalid signature" name="GitHub Controller should reject a webhook with invalid signature" time="0.017">
</testcase>
<testcase classname="GitHub Controller should ignore comments without @TestBot mention" name="GitHub Controller should ignore comments without @TestBot mention" time="0">
</testcase>
<testcase classname="GitHub Controller should handle errors from Claude service" name="GitHub Controller should handle errors from Claude service" time="0.002">
</testcase>
</testsuite>
<testsuite name="Claude Service" errors="0" failures="0" skipped="0" timestamp="2025-05-26T03:05:19" time="0.101" tests="4">
<testcase classname="Claude Service processCommand should handle test mode correctly" name="Claude Service processCommand should handle test mode correctly" time="0.001">
</testcase>
<testcase classname="Claude Service processCommand should properly set up Docker command in production mode" name="Claude Service processCommand should properly set up Docker command in production mode" time="0.001">
</testcase>
<testcase classname="Claude Service processCommand should handle errors properly" name="Claude Service processCommand should handle errors properly" time="0.008">
<testcase classname="Claude Service processCommand should handle errors properly" name="Claude Service processCommand should handle errors properly" time="0.011">
</testcase>
<testcase classname="Claude Service processCommand should handle long commands properly" name="Claude Service processCommand should handle long commands properly" time="0">
<testcase classname="Claude Service processCommand should handle long commands properly" name="Claude Service processCommand should handle long commands properly" time="0.001">
</testcase>
</testsuite>
<testsuite name="AWS Credential Provider" errors="0" failures="0" skipped="0" timestamp="2025-05-26T02:09:45" time="0.023" tests="7">
<testcase classname="AWS Credential Provider should get credentials from AWS profile" name="AWS Credential Provider should get credentials from AWS profile" time="0">
<testsuite name="AWS Credential Provider" errors="0" failures="0" skipped="0" timestamp="2025-05-26T03:05:20" time="0.035" tests="7">
<testcase classname="AWS Credential Provider should get credentials from AWS profile" name="AWS Credential Provider should get credentials from AWS profile" time="0.006">
</testcase>
<testcase classname="AWS Credential Provider should cache credentials" name="AWS Credential Provider should cache credentials" time="0">
<testcase classname="AWS Credential Provider should cache credentials" name="AWS Credential Provider should cache credentials" time="0.001">
</testcase>
<testcase classname="AWS Credential Provider should clear credential cache" name="AWS Credential Provider should clear credential cache" time="0">
<testcase classname="AWS Credential Provider should clear credential cache" name="AWS Credential Provider should clear credential cache" time="0.001">
</testcase>
<testcase classname="AWS Credential Provider should get Docker environment variables" name="AWS Credential Provider should get Docker environment variables" time="0.001">
<testcase classname="AWS Credential Provider should get Docker environment variables" name="AWS Credential Provider should get Docker environment variables" time="0">
</testcase>
<testcase classname="AWS Credential Provider should throw error if AWS_PROFILE is not set" name="AWS Credential Provider should throw error if AWS_PROFILE is not set" time="0.005">
<testcase classname="AWS Credential Provider should throw error if AWS_PROFILE is not set" name="AWS Credential Provider should throw error if AWS_PROFILE is not set" time="0.006">
</testcase>
<testcase classname="AWS Credential Provider should throw error for non-existent profile" name="AWS Credential Provider should throw error for non-existent profile" time="0">
<testcase classname="AWS Credential Provider should throw error for non-existent profile" name="AWS Credential Provider should throw error for non-existent profile" time="0.001">
</testcase>
<testcase classname="AWS Credential Provider should throw error for incomplete credentials" name="AWS Credential Provider should throw error for incomplete credentials" time="0.001">
</testcase>