Skip to content
Merged
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
103 changes: 103 additions & 0 deletions __tests__/credential-detection-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,107 @@ describe('Credential Detection Integration', () => {
assert.ok(shortKeyVar, 'Short key variable should exist')
assert.strictEqual(shortKeyVar.value, 'ab') // Too short to mask
})

it('should detect credentials in arguments similar to context7 example', () => {
// Test the exact case from the user's example
const args = ['-y', '@upstash/context7-mcp', '--api-key', 'TOKEN']

const credentials = CredentialDetectionService.analyzeArguments(args)

assert.strictEqual(credentials.hasCredentials, true)
assert.strictEqual(credentials.riskLevel, 'high')
assert.strictEqual(credentials.credentialVars.length, 1)

const credVar = credentials.credentialVars[0]
assert.strictEqual(credVar.name, '--api-key')
assert.strictEqual(credVar.value, 'T***N') // TOKEN masked
assert.strictEqual(credVar.riskLevel, 'high')
assert.strictEqual(credVar.source, 'args')
})

it('should detect credentials in HTTP headers with variable substitution handling', () => {
// Test case with mix of safe and unsafe headers
const headers = {
'Authorization': '${input:github_mcp_pat}', // Safe - variable substitution
'X-API-Key': 'real-api-key-123', // Unsafe - plain text
'Content-Type': 'application/json' // Not a credential
}

const credentials = CredentialDetectionService.analyzeHeaders(headers)

assert.strictEqual(credentials.hasCredentials, true)
assert.strictEqual(credentials.riskLevel, 'high')
assert.strictEqual(credentials.credentialVars.length, 1) // Only the unsafe one

const credVar = credentials.credentialVars[0]
assert.strictEqual(credVar.name, 'X-API-Key')
assert.strictEqual(credVar.value, 'r********3') // real-api-key-123 masked
assert.strictEqual(credVar.riskLevel, 'high')
assert.strictEqual(credVar.source, 'headers')
})

it('should handle comprehensive server config with all credential types', () => {
// Test a complete MCP server configuration with credentials in all places
const serverConfig = {
env: {
'OPENAI_API_KEY': 'sk-1234567890abcdef',
'OPENAI_ORG_ID': 'org-1234567890',
'NODE_ENV': 'production' // Not a credential
},
args: [
'npx', '-y', '@upstash/context7-mcp',
'--api-key', 'context7-token-123',
'--org-id', 'ctx7-org-456'
],
headers: {
'Authorization': 'Bearer github-pat-xyz789',
'X-Custom-Key': '${env:CUSTOM_KEY}', // Safe - variable substitution
'Content-Type': 'application/json' // Not a credential
}
}

const credentials = CredentialDetectionService.analyzeServerConfig(serverConfig)

assert.strictEqual(credentials.hasCredentials, true)
assert.strictEqual(credentials.riskLevel, 'high')
assert.strictEqual(credentials.credentialVars.length, 5)

// Check each source has the right number of credentials
const envVars = credentials.credentialVars.filter(v => v.source === 'env')
const argVars = credentials.credentialVars.filter(v => v.source === 'args')
const headerVars = credentials.credentialVars.filter(v => v.source === 'headers')

assert.strictEqual(envVars.length, 2) // OPENAI_API_KEY, OPENAI_ORG_ID
assert.strictEqual(argVars.length, 2) // --api-key, --org-id
assert.strictEqual(headerVars.length, 1) // Authorization (X-Custom-Key is safe)

// Verify risk levels
const highRiskVars = credentials.credentialVars.filter(v => v.riskLevel === 'high')
const lowRiskVars = credentials.credentialVars.filter(v => v.riskLevel === 'low')

assert.strictEqual(highRiskVars.length, 3) // OPENAI_API_KEY, --api-key, Authorization
assert.strictEqual(lowRiskVars.length, 2) // OPENAI_ORG_ID, --org-id
})

it('should handle edge cases with empty or missing data', () => {
// Test completely empty config
const emptyConfig = {}
const credentials1 = CredentialDetectionService.analyzeServerConfig(emptyConfig)

assert.strictEqual(credentials1.hasCredentials, false)
assert.strictEqual(credentials1.riskLevel, 'none')
assert.strictEqual(credentials1.credentialVars.length, 0)

// Test config with all empty arrays/objects
const emptyArraysConfig = {
env: {},
args: [],
headers: {}
}
const credentials2 = CredentialDetectionService.analyzeServerConfig(emptyArraysConfig)

assert.strictEqual(credentials2.hasCredentials, false)
assert.strictEqual(credentials2.riskLevel, 'none')
assert.strictEqual(credentials2.credentialVars.length, 0)
})
})
234 changes: 234 additions & 0 deletions __tests__/credential-detection-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,239 @@ describe('CredentialDetectionService', () => {
assert.strictEqual(orgIdVar?.riskLevel, 'low')
assert.strictEqual(result.riskLevel, 'high') // Overall risk should be high
})

it('should skip variable substitution patterns in environment variables', () => {
const env = {
'GITHUB_PERSONAL_ACCESS_TOKEN': '${input:github_token}',
'API_KEY': '${env:API_KEY}',
'REAL_TOKEN': 'sk-1234567890abcdef' // This should be detected
}

const result = CredentialDetectionService.analyzeEnvironmentVariables(env)
assert.strictEqual(result.hasCredentials, true)
assert.strictEqual(result.credentialVars.length, 1) // Only the REAL_TOKEN
assert.strictEqual(result.credentialVars[0].name, 'REAL_TOKEN')
assert.strictEqual(result.credentialVars[0].riskLevel, 'high')
})
})

describe('isVariableSubstitution', () => {
it('should detect variable substitution patterns', () => {
assert.strictEqual(CredentialDetectionService.isVariableSubstitution('${input:github_mcp_pat}'), true)
assert.strictEqual(CredentialDetectionService.isVariableSubstitution('${env:API_KEY}'), true)
assert.strictEqual(CredentialDetectionService.isVariableSubstitution('${config:token}'), true)
assert.strictEqual(CredentialDetectionService.isVariableSubstitution('${secret:key}'), true)
})

it('should not detect plain text as variable substitution', () => {
assert.strictEqual(CredentialDetectionService.isVariableSubstitution('plain-text-token'), false)
assert.strictEqual(CredentialDetectionService.isVariableSubstitution('sk-1234567890abcdef'), false)
assert.strictEqual(CredentialDetectionService.isVariableSubstitution('Bearer token123'), false)
assert.strictEqual(CredentialDetectionService.isVariableSubstitution('${incomplete'), false)
assert.strictEqual(CredentialDetectionService.isVariableSubstitution('incomplete}'), false)
})
})

describe('analyzeArguments', () => {
it('should return no credentials for undefined args', () => {
const result = CredentialDetectionService.analyzeArguments(undefined)
assert.strictEqual(result.hasCredentials, false)
assert.strictEqual(result.credentialVars.length, 0)
assert.strictEqual(result.riskLevel, 'none')
})

it('should return no credentials for empty args', () => {
const result = CredentialDetectionService.analyzeArguments([])
assert.strictEqual(result.hasCredentials, false)
assert.strictEqual(result.credentialVars.length, 0)
assert.strictEqual(result.riskLevel, 'none')
})

it('should detect API key arguments', () => {
const args = ['-y', '@upstash/context7-mcp', '--api-key', 'TOKEN123']
const result = CredentialDetectionService.analyzeArguments(args)

assert.strictEqual(result.hasCredentials, true)
assert.strictEqual(result.credentialVars.length, 1)
assert.strictEqual(result.credentialVars[0].name, '--api-key')
assert.strictEqual(result.credentialVars[0].value, 'T******3')
assert.strictEqual(result.credentialVars[0].riskLevel, 'high')
assert.strictEqual(result.credentialVars[0].source, 'args')
assert.strictEqual(result.riskLevel, 'high')
})

it('should detect multiple credential arguments', () => {
const args = ['command', '--api-key', 'key123', '--password', 'secret456', '--org-id', 'org789']
const result = CredentialDetectionService.analyzeArguments(args)

assert.strictEqual(result.hasCredentials, true)
assert.strictEqual(result.credentialVars.length, 3)

const apiKeyVar = result.credentialVars.find(v => v.name === '--api-key')
const passwordVar = result.credentialVars.find(v => v.name === '--password')
const orgIdVar = result.credentialVars.find(v => v.name === '--org-id')

assert.strictEqual(apiKeyVar?.riskLevel, 'high')
assert.strictEqual(passwordVar?.riskLevel, 'high')
assert.strictEqual(orgIdVar?.riskLevel, 'low')
assert.strictEqual(result.riskLevel, 'high')
})

it('should handle various argument patterns', () => {
const args = ['--token', 'abc123', '--auth', 'def456', '--secret', 'ghi789']
const result = CredentialDetectionService.analyzeArguments(args)

assert.strictEqual(result.hasCredentials, true)
assert.strictEqual(result.credentialVars.length, 3)
result.credentialVars.forEach(v => {
assert.strictEqual(v.riskLevel, 'high')
assert.strictEqual(v.source, 'args')
})
})

it('should not detect non-credential arguments', () => {
const args = ['--help', '--version', '--port', '3000', '--verbose']
const result = CredentialDetectionService.analyzeArguments(args)

assert.strictEqual(result.hasCredentials, false)
assert.strictEqual(result.credentialVars.length, 0)
assert.strictEqual(result.riskLevel, 'none')
})

it('should handle arguments without values', () => {
const args = ['--api-key'] // No value after the flag
const result = CredentialDetectionService.analyzeArguments(args)

assert.strictEqual(result.hasCredentials, false)
assert.strictEqual(result.credentialVars.length, 0)
assert.strictEqual(result.riskLevel, 'none')
})

it('should handle flag-like values', () => {
const args = ['--api-key', '--another-flag'] // Next arg is also a flag
const result = CredentialDetectionService.analyzeArguments(args)

assert.strictEqual(result.hasCredentials, false)
assert.strictEqual(result.credentialVars.length, 0)
assert.strictEqual(result.riskLevel, 'none')
})
})

describe('analyzeHeaders', () => {
it('should return no credentials for undefined headers', () => {
const result = CredentialDetectionService.analyzeHeaders(undefined)
assert.strictEqual(result.hasCredentials, false)
assert.strictEqual(result.credentialVars.length, 0)
assert.strictEqual(result.riskLevel, 'none')
})

it('should detect Authorization header with plain text', () => {
const headers = {
'Authorization': 'Bearer sk-1234567890abcdef',
'Content-Type': 'application/json'
}
const result = CredentialDetectionService.analyzeHeaders(headers)

assert.strictEqual(result.hasCredentials, true)
assert.strictEqual(result.credentialVars.length, 1)
assert.strictEqual(result.credentialVars[0].name, 'Authorization')
assert.strictEqual(result.credentialVars[0].value, 'B********f')
assert.strictEqual(result.credentialVars[0].riskLevel, 'high')
assert.strictEqual(result.credentialVars[0].source, 'headers')
})

it('should skip variable substitution in headers', () => {
const headers = {
'Authorization': '${input:github_mcp_pat}',
'X-API-Key': '${env:API_KEY}'
}
const result = CredentialDetectionService.analyzeHeaders(headers)

assert.strictEqual(result.hasCredentials, false)
assert.strictEqual(result.credentialVars.length, 0)
assert.strictEqual(result.riskLevel, 'none')
})

it('should detect multiple credential headers', () => {
const headers = {
'Authorization': 'Bearer token123',
'X-API-Key': 'key456',
'X-Auth-Token': 'auth789',
'X-Org-ID': 'org123',
'Content-Type': 'application/json'
}
const result = CredentialDetectionService.analyzeHeaders(headers)

assert.strictEqual(result.hasCredentials, true)
assert.strictEqual(result.credentialVars.length, 4)

const authVar = result.credentialVars.find(v => v.name === 'Authorization')
const apiKeyVar = result.credentialVars.find(v => v.name === 'X-API-Key')
const authTokenVar = result.credentialVars.find(v => v.name === 'X-Auth-Token')
const orgIdVar = result.credentialVars.find(v => v.name === 'X-Org-ID')

assert.strictEqual(authVar?.riskLevel, 'high')
assert.strictEqual(apiKeyVar?.riskLevel, 'high')
assert.strictEqual(authTokenVar?.riskLevel, 'high')
assert.strictEqual(orgIdVar?.riskLevel, 'low')
assert.strictEqual(result.riskLevel, 'high')
})

it('should handle mixed safe and unsafe headers', () => {
const headers = {
'Authorization': '${input:token}', // Safe - variable substitution
'X-API-Key': 'real-key-123', // Unsafe - plain text
'Content-Type': 'application/json'
}
const result = CredentialDetectionService.analyzeHeaders(headers)

assert.strictEqual(result.hasCredentials, true)
assert.strictEqual(result.credentialVars.length, 1)
assert.strictEqual(result.credentialVars[0].name, 'X-API-Key')
assert.strictEqual(result.credentialVars[0].riskLevel, 'high')
})
})

describe('analyzeServerConfig', () => {
it('should combine analysis from all sources', () => {
const config = {
env: { 'API_KEY': 'env-key-123' },
args: ['--token', 'arg-token-456'],
headers: { 'Authorization': 'Bearer header-auth-789' }
}
const result = CredentialDetectionService.analyzeServerConfig(config)

assert.strictEqual(result.hasCredentials, true)
assert.strictEqual(result.credentialVars.length, 3)

const envVar = result.credentialVars.find(v => v.source === 'env')
const argsVar = result.credentialVars.find(v => v.source === 'args')
const headersVar = result.credentialVars.find(v => v.source === 'headers')

assert.strictEqual(envVar?.name, 'API_KEY')
assert.strictEqual(argsVar?.name, '--token')
assert.strictEqual(headersVar?.name, 'Authorization')
assert.strictEqual(result.riskLevel, 'high')
})

it('should handle empty config', () => {
const result = CredentialDetectionService.analyzeServerConfig({})

assert.strictEqual(result.hasCredentials, false)
assert.strictEqual(result.credentialVars.length, 0)
assert.strictEqual(result.riskLevel, 'none')
})

it('should prioritize highest risk level', () => {
const config = {
env: { 'ORG_ID': 'org123' }, // Low risk
headers: { 'Authorization': '${input:token}' } // Safe - should be ignored
}
const result = CredentialDetectionService.analyzeServerConfig(config)

assert.strictEqual(result.hasCredentials, true)
assert.strictEqual(result.credentialVars.length, 1)
assert.strictEqual(result.riskLevel, 'low')
})
})
})
Loading
Loading