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
28 changes: 28 additions & 0 deletions packages/build/src/plugins_core/secrets_scanning/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,30 @@ export function getSecretKeysToScanFor(env: Record<string, unknown>, secretKeys:
return filteredSecretKeys.filter((key) => !isValueTrivial(env[key]))
}

const getShannonEntropy = (str: string): number => {
const len = str.length
if (len === 0) return 0

const freqMap = {}
for (const char of str) {
freqMap[char] = (freqMap[char] || 0) + 1
}

let entropy = 0
for (const char in freqMap) {
const p = freqMap[char] / len
entropy -= p * Math.log2(p)
}

return entropy
}

const HIGH_ENTROPY_THRESHOLD = 4.5
const doesEntropyMeetThresholdForSecret = (str: string): boolean => {
const entropy = getShannonEntropy(str)
return entropy >= HIGH_ENTROPY_THRESHOLD
}

// Most prefixes are 4-5 chars, so requiring 12 chars after ensures a reasonable secret length
const MIN_CHARS_AFTER_PREFIX = 12

Expand Down Expand Up @@ -187,6 +211,10 @@ export function findLikelySecrets({
if (!token || !prefix || allOmittedValues.includes(token)) {
continue
}
// Despite the prefix, the string does not look random enough to be convinced it's a secret
if (!doesEntropyMeetThresholdForSecret(token)) {
continue
}
matches.push({
prefix,
index: match.index,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[build.environment]
ENV_VAR_1 = "sk_12345678901234567890"
ENV_VAR_1 = "sk_dF6gH9jK4mP7nW2xR5tYc6dBmFP5ym"
ENV_VAR_2 = "val2-val2-val2"
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[build.environment]
SECRETS_SCAN_SMART_DETECTION_ENABLED = "false"
ENV_VAR_1 = "sk_12345678901234567890"
ENV_VAR_1 = "sk_dF6gH9jK4mP7nW2xR5tYc6dBmFP5ym"
ENV_VAR_2 = "val2-val2-val2"
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[build.environment]
ENV_VAR_1 = "sk_12345678901234567890"
ENV_VAR_1 = "sk_dF6gH9jK4mP7nW2xR5tYc6dBmFP5ym"
ENV_VAR_2 = "val2-val2-val2"
SECRETS_SCAN_SMART_DETECTION_OMIT_VALUES = "sk_12345678901234567890"
SECRETS_SCAN_SMART_DETECTION_OMIT_VALUES = "sk_dF6gH9jK4mP7nW2xR5tYc6dBmFP5ym"
54 changes: 28 additions & 26 deletions packages/build/tests/utils_secretscanning/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { findLikelySecrets } from '../../lib/plugins_core/secrets_scanning/utils

test('findLikelySecrets - should not find secrets without quotes or delimiters', async (t) => {
const lines = [
'aws_123456789012345678',
'ghp_1234567890123456789',
'xoxb-123456789012345678',
'nf_123456789012345678',
'aws_Kj2P9xL5mN8vB3cX7qA4',
'ghp_zR4wY7hQ9sK2nM5vL8xbkokM0vgXC',
'xoxb-bV8cX3zL6kM9nQ4wR7y3FIASwY6YX',
'nf_pT2hN7mK4jL9wB5vC8xOzHucej7Or',
]

lines.forEach((text) => {
Expand All @@ -18,12 +18,12 @@ test('findLikelySecrets - should not find secrets without quotes or delimiters',

test('findLikelySecrets - should find secrets with quotes or equals', async (t) => {
const matchingLines = [
'my_secret_key=aws_123456789012345678',
'mySecretKey = aws_123456789012345678',
'secretKey="aws_123456789012345678"',
'secretKey = "aws_123456789012345678"',
"secretKey='aws_123456789012345678'",
'secretKey=`aws_123456789012345678`',
'my_secret_key=aws_Kj2P9xL5mN8vB3cX7qA4',
'mySecretKey = aws_zR4wY7hQ9sK2nM5vL8xbkokM0vgXC',
'secretKey="aws_dF6gH9jK4mP7nW2xR5tYc6dBmFP5ym"',
'secretKey = "aws_bV8cX3zL6kM9nQ4wR7y3FIASwY6YX"',
"secretKey='aws_pT2hN7mK4jL9wB5vC8xOzHucej7Or'",
'secretKey=`aws_qS3fD8gH5jK2mN6pR9yHfBxkujdx2`',
]
matchingLines.forEach((text) => {
const matches = findLikelySecrets({ text })
Expand All @@ -32,18 +32,18 @@ test('findLikelySecrets - should find secrets with quotes or equals', async (t)
})

test('findLikelySecrets - should not match values with spaces after prefix', async (t) => {
const nonMatchingLine = 'aws_ "123456789012345678"'
const nonMatchingLine = 'aws_ "Kj2P9xL5mN8vB3cX7qA4"'
const matches = findLikelySecrets({ text: nonMatchingLine })
t.is(matches.length, 0)
})

test('findLikelySecrets - should not match values that are too short', async (t) => {
const matches = findLikelySecrets({ text: 'aws_key="12345678901"' })
const matches = findLikelySecrets({ text: 'aws_key="aws_x7B9nM4k"' })
t.is(matches.length, 0)
})

test('findLikelySecrets - should return the matched prefix as the key', async (t) => {
const matches = findLikelySecrets({ text: 'mykey = "github_pat_123456789012345678"' })
const matches = findLikelySecrets({ text: 'mykey = "github_pat_Kj2P9xL5mN8vB3cX7qA4"' })
t.is(matches.length, 1)
t.is(matches[0].prefix, 'github_pat_')
})
Expand All @@ -57,14 +57,12 @@ test('findLikelySecrets - should handle empty or invalid input', async (t) => {
}
})

test('findLikelySecrets - should match exactly minimum chars after prefix', async (t) => {
const exactMinChars = 'value = "aws_123456789012"' // Exactly 12 chars after prefix
const matches = findLikelySecrets({ text: exactMinChars })
t.is(matches.length, 1)
})

test('findLikelySecrets - should match different prefixes from LIKELY_SECRET_PREFIXES', async (t) => {
const lines = ['key="ghp_123456789012345678"', 'key="sk_123456789012345678"', 'key="aws_123456789012345678"']
const lines = [
'key="ghp_zR4wY7hQ9sK2nM5vL8xbkokM0vgX"',
'key="sk_zR4wY7hQ9sK2nM5vL8xbkokM0vgX"',
'key="aws_zR4wY7hQ9sK2nM5vL8xbkokM0vgX"',
]

lines.forEach((text) => {
const matches = findLikelySecrets({ text })
Expand All @@ -79,14 +77,18 @@ test('findLikelySecrets - should skip safe-listed values', async (t) => {
})

test('findLikelySecrets - should allow dashes and alphanumeric characters only', async (t) => {
const validLines = ['key="aws_abc123-456-789"', 'key="ghp_abc-123-def-456"']
const validLines = ['key="aws_zR4wY7hQ-9sK2nM5vL8xbko-kM0vgXKj2P"', 'key="ghp_zR4wY7hQ9sK2n-M5vL8xbkokM0vgX"']

validLines.forEach((line) => {
const matches = findLikelySecrets({ text: line })
t.is(matches.length, 1, `Should match line with dashes: ${line}`)
})

const invalidLines = ['key="aws_abc123!@#$%^&*()_+"', 'key="ghp_abc.123_456.789"', 'key="sk_live_123_456_789"']
const invalidLines = [
'key="aws_zR4wY7hQ9sK2nMgX!@#$%^&*()_+"',
'key="ghp_zR4wY7hQ.9sK2nM5vL8.xbkokM0vgX"',
'key="sk_R4_wY7hQ9sK2_nM5vL8xbkokM0vgX"',
]

invalidLines.forEach((line) => {
const matches = findLikelySecrets({ text: line })
Expand All @@ -97,16 +99,16 @@ test('findLikelySecrets - should allow dashes and alphanumeric characters only',
test('findLikelySecrets - should match full secret value against omitValues', async (t) => {
// Test both partial and full matches to ensure proper behavior
const partialMatch = findLikelySecrets({
text: 'key="aws_123456789012extracharshere"',
text: 'key="aws_zR4wY7hQ9sK2nM5vL8xbkokM0vgX"',
// The omitValue only partially matches the secret - we should still detect the secret
omitValuesFromEnhancedScan: ['aws_123456789012'],
omitValuesFromEnhancedScan: ['aws_zR4wY7hQ9'],
})
t.is(partialMatch.length, 1)

const fullMatch = findLikelySecrets({
text: 'key="aws_123456789012extracharshere"',
text: 'key="aws_zR4wY7hQ9sK2nM5vL8xbkokM0vgX"',
// Omit the full secret value - we should not detect the secret
omitValuesFromEnhancedScan: ['aws_123456789012extracharshere'],
omitValuesFromEnhancedScan: ['aws_zR4wY7hQ9sK2nM5vL8xbkokM0vgX'],
})
t.is(fullMatch.length, 0)
})
Loading