Files
aquasecurity-trivy/pkg/fanal/secret/scanner_test.go

1938 lines
64 KiB
Go
Raw Blame History

package secret_test
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/aquasecurity/trivy/pkg/fanal/secret"
"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
)
func TestMain(m *testing.M) {
log.InitLogger(false, true)
os.Exit(m.Run())
}
func TestSecretScanner(t *testing.T) {
wantFinding1 := types.SecretFinding{
RuleID: "rule1",
Category: "general",
Title: "Generic Rule",
Severity: "HIGH",
StartLine: 2,
EndLine: 2,
Match: "generic secret line secret=\"*********\"",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "--- ignore block start ---",
Highlighted: "--- ignore block start ---",
},
{
Number: 2,
Content: "generic secret line secret=\"*********\"",
Highlighted: "generic secret line secret=\"*********\"",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 3,
Content: "--- ignore block stop ---",
Highlighted: "--- ignore block stop ---",
},
},
},
Offset: 55,
}
wantFinding2 := types.SecretFinding{
RuleID: "rule1",
Category: "general",
Title: "Generic Rule",
Severity: "HIGH",
StartLine: 4,
EndLine: 4,
Match: "secret=\"**********\"",
Code: types.Code{
Lines: []types.Line{
{
Number: 2,
Content: "generic secret line secret=\"*********\"",
Highlighted: "generic secret line secret=\"*********\"",
},
{
Number: 3,
Content: "--- ignore block stop ---",
Highlighted: "--- ignore block stop ---",
},
{
Number: 4,
Content: "secret=\"**********\"",
Highlighted: "secret=\"**********\"",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 5,
Content: "credentials: { user: \"username\" password: \"123456789\" }",
Highlighted: "credentials: { user: \"username\" password: \"123456789\" }",
},
},
},
Offset: 100,
}
wantFindingRegexDisabled := types.SecretFinding{
RuleID: "rule1",
Category: "general",
Title: "Generic Rule",
Severity: "HIGH",
StartLine: 4,
EndLine: 4,
Match: "secret=\"**********\"",
Code: types.Code{
Lines: []types.Line{
{
Number: 2,
Content: "generic secret line secret=\"somevalue\"",
Highlighted: "generic secret line secret=\"somevalue\"",
},
{
Number: 3,
Content: "--- ignore block stop ---",
Highlighted: "--- ignore block stop ---",
},
{
Number: 4,
Content: "secret=\"**********\"",
Highlighted: "secret=\"**********\"",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 5,
Content: "credentials: { user: \"username\" password: \"123456789\" }",
Highlighted: "credentials: { user: \"username\" password: \"123456789\" }",
},
},
},
Offset: 100,
}
wantFindingMultipleGroupsUsername := types.SecretFinding{
RuleID: "rule1",
Category: "general",
Title: "Generic Rule",
Severity: "HIGH",
StartLine: 5,
EndLine: 5,
Match: "credentials: { user: \"********\" password: \"*********\" }",
Code: types.Code{
Lines: []types.Line{
{
Number: 3,
Content: "--- ignore block stop ---",
Highlighted: "--- ignore block stop ---",
},
{
Number: 4,
Content: "secret=\"othervalue\"",
Highlighted: "secret=\"othervalue\"",
},
{
Number: 5,
Content: "credentials: { user: \"********\" password: \"*********\" }",
Highlighted: "credentials: { user: \"********\" password: \"*********\" }",
IsCause: true,
FirstCause: true,
LastCause: true,
},
},
},
Offset: 134,
}
wantFindingMultipleGroupsPassword := types.SecretFinding{
RuleID: "rule1",
Category: "general",
Title: "Generic Rule",
Severity: "HIGH",
StartLine: 5,
EndLine: 5,
Match: "credentials: { user: \"********\" password: \"*********\" }",
Code: types.Code{
Lines: []types.Line{
{
Number: 3,
Content: "--- ignore block stop ---",
Highlighted: "--- ignore block stop ---",
},
{
Number: 4,
Content: "secret=\"othervalue\"",
Highlighted: "secret=\"othervalue\"",
},
{
Number: 5,
Content: "credentials: { user: \"********\" password: \"*********\" }",
Highlighted: "credentials: { user: \"********\" password: \"*********\" }",
IsCause: true,
FirstCause: true,
LastCause: true,
},
},
},
Offset: 155,
}
wantFinding5 := types.SecretFinding{
RuleID: "aws-access-key-id",
Category: secret.CategoryAWS,
Title: "AWS Access Key ID",
Severity: "CRITICAL",
StartLine: 2,
EndLine: 2,
Match: "AWS_ACCESS_KEY_ID=********************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "'AWS_secret_KEY'=\"****************************************\"",
Highlighted: "'AWS_secret_KEY'=\"****************************************\"",
},
{
Number: 2,
Content: "AWS_ACCESS_KEY_ID=********************",
Highlighted: "AWS_ACCESS_KEY_ID=********************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 3,
Content: "\"aws_account_ID\":'1234-5678-9123'",
Highlighted: "\"aws_account_ID\":'1234-5678-9123'",
},
},
},
Offset: 78,
}
wantFinding5a := types.SecretFinding{
RuleID: "aws-access-key-id",
Category: secret.CategoryAWS,
Title: "AWS Access Key ID",
Severity: "CRITICAL",
StartLine: 2,
EndLine: 2,
Match: "AWS_ACCESS_KEY_ID=********************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "GITHUB_PAT=****************************************",
Highlighted: "GITHUB_PAT=****************************************",
},
{
Number: 2,
Content: "AWS_ACCESS_KEY_ID=********************",
Highlighted: "AWS_ACCESS_KEY_ID=********************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
},
},
Offset: 70,
}
wantFindingPATDisabled := types.SecretFinding{
RuleID: "aws-access-key-id",
Category: secret.CategoryAWS,
Title: "AWS Access Key ID",
Severity: "CRITICAL",
StartLine: 2,
EndLine: 2,
Match: "AWS_ACCESS_KEY_ID=********************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "GITHUB_PAT=ghp_012345678901234567890123456789abcdef",
Highlighted: "GITHUB_PAT=ghp_012345678901234567890123456789abcdef",
},
{
Number: 2,
Content: "AWS_ACCESS_KEY_ID=********************",
Highlighted: "AWS_ACCESS_KEY_ID=********************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
},
},
Offset: 70,
}
wantFinding6 := types.SecretFinding{
RuleID: "github-pat",
Category: secret.CategoryGitHub,
Title: "GitHub Personal Access Token",
Severity: "CRITICAL",
StartLine: 1,
EndLine: 1,
Match: "GITHUB_PAT=****************************************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "GITHUB_PAT=****************************************",
Highlighted: "GITHUB_PAT=****************************************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 2,
Content: "AWS_ACCESS_KEY_ID=********************",
Highlighted: "AWS_ACCESS_KEY_ID=********************",
},
},
},
Offset: 11,
}
wantFindingGitHubPAT := types.SecretFinding{
RuleID: "github-fine-grained-pat",
Category: secret.CategoryGitHub,
Title: "GitHub Fine-grained personal access tokens",
Severity: "CRITICAL",
StartLine: 1,
EndLine: 1,
Match: "GITHUB_TOKEN=*********************************************************************************************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "GITHUB_TOKEN=*********************************************************************************************",
Highlighted: "GITHUB_TOKEN=*********************************************************************************************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
},
},
Offset: 13,
}
wantFindingMyAwsAccessKey := types.SecretFinding{
RuleID: "aws-secret-access-key",
Category: secret.CategoryAWS,
Title: "AWS Secret Access Key",
Severity: "CRITICAL",
StartLine: 1,
EndLine: 1,
Match: `MyAWS_secret_KEY="****************************************"`,
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "MyAWS_secret_KEY=\"****************************************\"",
Highlighted: "MyAWS_secret_KEY=\"****************************************\"",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 2,
Content: "our*********************************************************************************************",
Highlighted: "our*********************************************************************************************",
},
},
},
Offset: 18,
}
wantFindingMyGitHubPAT := types.SecretFinding{
RuleID: "github-fine-grained-pat",
Category: secret.CategoryGitHub,
Title: "GitHub Fine-grained personal access tokens",
Severity: "CRITICAL",
StartLine: 2,
EndLine: 2,
Match: "our*********************************************************************************************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "MyAWS_secret_KEY=\"****************************************\"",
Highlighted: "MyAWS_secret_KEY=\"****************************************\"",
},
{
Number: 2,
Content: "our*********************************************************************************************",
Highlighted: "our*********************************************************************************************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
},
},
Offset: 63,
}
wantFindingGHButDisableAWS := types.SecretFinding{
RuleID: "github-pat",
Category: secret.CategoryGitHub,
Title: "GitHub Personal Access Token",
Severity: "CRITICAL",
StartLine: 1,
EndLine: 1,
Match: "GITHUB_PAT=****************************************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "GITHUB_PAT=****************************************",
Highlighted: "GITHUB_PAT=****************************************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 2,
Content: "AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF",
Highlighted: "AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF",
},
},
},
Offset: 11,
}
wantFinding7 := types.SecretFinding{
RuleID: "github-pat",
Category: secret.CategoryGitHub,
Title: "GitHub Personal Access Token",
Severity: "CRITICAL",
StartLine: 1,
EndLine: 1,
Match: "aaaaaaaaaaaaaaaaaa GITHUB_PAT=**************************************** bbbbbbbbbbbbbbbbbbb",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "aaaaaaaaaaaaaaaaaa GITHUB_PAT=**************************************** bbbbbbbbbbbbbbbbbbb",
Highlighted: "aaaaaaaaaaaaaaaaaa GITHUB_PAT=**************************************** bbbbbbbbbbbbbbbbbbb",
IsCause: true,
FirstCause: true,
LastCause: true,
},
},
},
Offset: 67,
}
wantFinding8 := types.SecretFinding{
RuleID: "rule1",
Category: "general",
Title: "Generic Rule",
Severity: "UNKNOWN",
StartLine: 2,
EndLine: 2,
Match: "generic secret line secret=\"*********\"",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "--- ignore block start ---",
Highlighted: "--- ignore block start ---",
},
{
Number: 2,
Content: "generic secret line secret=\"*********\"",
Highlighted: "generic secret line secret=\"*********\"",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 3,
Content: "--- ignore block stop ---",
Highlighted: "--- ignore block stop ---",
},
},
},
Offset: 55,
}
wantFinding9 := types.SecretFinding{
RuleID: "aws-secret-access-key",
Category: secret.CategoryAWS,
Title: "AWS Secret Access Key",
Severity: "CRITICAL",
StartLine: 1,
EndLine: 1,
Match: `'AWS_secret_KEY'="****************************************"`,
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "'AWS_secret_KEY'=\"****************************************\"",
Highlighted: "'AWS_secret_KEY'=\"****************************************\"",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 2,
Content: "AWS_ACCESS_KEY_ID=********************",
Highlighted: "AWS_ACCESS_KEY_ID=********************",
},
},
},
Offset: 18,
}
wantFinding10 := types.SecretFinding{
RuleID: "aws-secret-access-key",
Category: secret.CategoryAWS,
Title: "AWS Secret Access Key",
Severity: "CRITICAL",
StartLine: 5,
EndLine: 5,
Match: ` "created_by": "ENV aws_sec_key "****************************************",`,
Code: types.Code{
Lines: []types.Line{
{
Number: 3,
Content: "\"aws_account_ID\":'1234-5678-9123'",
Highlighted: "\"aws_account_ID\":'1234-5678-9123'",
},
{
Number: 4,
Content: "AWS_example=AKIAIOSFODNN7EXAMPLE",
Highlighted: "AWS_example=AKIAIOSFODNN7EXAMPLE",
},
{
Number: 5,
Content: " \"created_by\": \"ENV aws_sec_key \"****************************************\",",
Highlighted: " \"created_by\": \"ENV aws_sec_key \"****************************************\",",
IsCause: true,
FirstCause: true,
LastCause: true,
},
},
},
Offset: 200,
}
wantFindingAsymmetricPrivateKeyJson := types.SecretFinding{
RuleID: "private-key",
Category: secret.CategoryAsymmetricPrivateKey,
Title: "Asymmetric Private Key",
Severity: "HIGH",
StartLine: 1,
EndLine: 1,
Match: "----BEGIN RSA PRIVATE KEY-----**************************************************************************************************************************-----END RSA PRIVATE",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "----BEGIN RSA PRIVATE KEY-----**************************************************************************************************************************-----END RSA PRIVATE",
Highlighted: "----BEGIN RSA PRIVATE KEY-----**************************************************************************************************************************-----END RSA PRIVATE",
IsCause: true,
FirstCause: true,
LastCause: true,
},
},
},
Offset: 40,
}
wantFindingAsymmetricPrivateKey := types.SecretFinding{
RuleID: "private-key",
Category: secret.CategoryAsymmetricPrivateKey,
Title: "Asymmetric Private Key",
Severity: "HIGH",
StartLine: 2,
EndLine: 4,
Match: "****************************************************************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "-----BEGIN RSA PRIVATE KEY-----",
Highlighted: "-----BEGIN RSA PRIVATE KEY-----",
},
{
Number: 2,
Content: "****************************************************************",
Highlighted: "****************************************************************",
IsCause: true,
FirstCause: true,
LastCause: false,
},
{
Number: 3,
Content: "****************************************************************",
Highlighted: "****************************************************************",
IsCause: true,
FirstCause: false,
LastCause: false,
},
{
Number: 4,
Content: "****************************************************",
Highlighted: "****************************************************",
IsCause: true,
FirstCause: false,
LastCause: true,
},
{
Number: 5,
Content: "-----END RSA PRIVATE KEY-----",
Highlighted: "-----END RSA PRIVATE KEY-----",
},
},
},
Offset: 32,
}
wantFindingAsymmSecretKey := types.SecretFinding{
RuleID: "private-key",
Category: secret.CategoryAsymmetricPrivateKey,
Title: "Asymmetric Private Key",
Severity: "HIGH",
StartLine: 2,
EndLine: 3,
Match: "***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "-----BEGIN RSA PRIVATE KEY-----",
Highlighted: "-----BEGIN RSA PRIVATE KEY-----",
IsCause: false,
FirstCause: false,
LastCause: false,
},
{
Number: 2,
Content: "***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************",
Highlighted: "***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************",
IsCause: true,
FirstCause: true,
LastCause: false,
},
{
Number: 3,
Content: "********************************************************************",
Highlighted: "********************************************************************",
IsCause: true,
FirstCause: false,
LastCause: true,
},
{
Number: 4,
Content: "-----END RSA PRIVATE KEY-----",
Highlighted: "-----END RSA PRIVATE KEY-----",
IsCause: false,
FirstCause: false,
LastCause: false,
},
},
},
Offset: 32,
}
wantFindingMinimumAsymmSecretKey := types.SecretFinding{
RuleID: "private-key",
Category: secret.CategoryAsymmetricPrivateKey,
Title: "Asymmetric Private Key",
Severity: "HIGH",
StartLine: 9,
EndLine: 9,
Match: "-----BEGIN PRIVATE KEY-----********************************-----END PRIVATE KEY-----",
Code: types.Code{
Lines: []types.Line{
{
Number: 7,
Content: "",
Highlighted: "",
IsCause: false,
FirstCause: false,
LastCause: false,
},
{
Number: 8,
Content: "## Theoretically Asymmetric Private Key of minimum length (RSA 128 bits)",
Highlighted: "## Theoretically Asymmetric Private Key of minimum length (RSA 128 bits)",
IsCause: false,
FirstCause: false,
LastCause: false,
},
{
Number: 9,
Content: "-----BEGIN PRIVATE KEY-----********************************-----END PRIVATE KEY-----",
Highlighted: "-----BEGIN PRIVATE KEY-----********************************-----END PRIVATE KEY-----",
IsCause: true,
FirstCause: true,
LastCause: true,
},
},
},
Offset: 1842,
}
wantFindingAlibabaAccessKeyId := types.SecretFinding{
RuleID: "alibaba-access-key-id",
Category: secret.CategoryAlibaba,
Title: "Alibaba AccessKey ID",
Severity: "HIGH",
StartLine: 2,
EndLine: 2,
Match: "key = ************************,",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "key : LTAI1234567890ABCDEFG123asd",
Highlighted: "key : LTAI1234567890ABCDEFG123asd",
},
{
Number: 2,
Content: "key = ************************,",
Highlighted: "key = ************************,",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 3,
Content: "asdLTAI1234567890ABCDEFG123",
Highlighted: "asdLTAI1234567890ABCDEFG123",
},
},
},
Offset: 40,
}
wantFindingDockerKey1 := types.SecretFinding{
RuleID: "dockerconfig-secret",
Category: secret.CategoryDocker,
Title: "Dockerconfig secret exposed",
Severity: "HIGH",
StartLine: 4,
EndLine: 4,
Match: " .dockercfg: ************",
Code: types.Code{
Lines: []types.Line{
{
Number: 2,
Content: " .dockerconfigjson: ************",
Highlighted: " .dockerconfigjson: ************",
},
{
Number: 3,
Content: "data2:",
Highlighted: "data2:",
},
{
Number: 4,
Content: " .dockercfg: ************",
Highlighted: " .dockercfg: ************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
},
},
Offset: 62,
}
wantFindingDockerKey2 := types.SecretFinding{
RuleID: "dockerconfig-secret",
Category: secret.CategoryDocker,
Title: "Dockerconfig secret exposed",
Severity: "HIGH",
StartLine: 2,
EndLine: 2,
Match: " .dockerconfigjson: ************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "data1:",
Highlighted: "data1:",
},
{
Number: 2,
Content: " .dockerconfigjson: ************",
Highlighted: " .dockerconfigjson: ************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 3,
Content: "data2:",
Highlighted: "data2:",
},
},
},
Offset: 28,
}
wantFindingPrivatePackagistOrgReadToken := types.SecretFinding{
RuleID: "private-packagist-token",
Category: secret.CategoryPrivatePackagist,
Title: "Private Packagist token",
Severity: "HIGH",
StartLine: 1,
EndLine: 1,
Match: "ORG_READ_TOKEN=**********************************************************************************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "ORG_READ_TOKEN=**********************************************************************************",
Highlighted: "ORG_READ_TOKEN=**********************************************************************************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 2,
Content: "ORG_WRITE_TOKEN=**********************************************************************************",
Highlighted: "ORG_WRITE_TOKEN=**********************************************************************************",
IsCause: false,
FirstCause: false,
LastCause: false,
},
},
},
Offset: 15,
}
wantFindingPrivatePackagistOrgUpdateToken := types.SecretFinding{
RuleID: "private-packagist-token",
Category: secret.CategoryPrivatePackagist,
Title: "Private Packagist token",
Severity: "HIGH",
StartLine: 2,
EndLine: 2,
Match: "ORG_WRITE_TOKEN=**********************************************************************************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "ORG_READ_TOKEN=**********************************************************************************",
Highlighted: "ORG_READ_TOKEN=**********************************************************************************",
IsCause: false,
FirstCause: false,
LastCause: false,
},
{
Number: 2,
Content: "ORG_WRITE_TOKEN=**********************************************************************************",
Highlighted: "ORG_WRITE_TOKEN=**********************************************************************************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 3,
Content: "USER_TOKEN=**********************************************************************************",
Highlighted: "USER_TOKEN=**********************************************************************************",
IsCause: false,
FirstCause: false,
LastCause: false,
},
},
},
Offset: 114,
}
wantFindingPrivatePackagistUserToken := types.SecretFinding{
RuleID: "private-packagist-token",
Category: secret.CategoryPrivatePackagist,
Title: "Private Packagist token",
Severity: "HIGH",
StartLine: 3,
EndLine: 3,
Match: "USER_TOKEN=**********************************************************************************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "ORG_READ_TOKEN=**********************************************************************************",
Highlighted: "ORG_READ_TOKEN=**********************************************************************************",
IsCause: false,
FirstCause: false,
LastCause: false,
},
{
Number: 2,
Content: "ORG_WRITE_TOKEN=**********************************************************************************",
Highlighted: "ORG_WRITE_TOKEN=**********************************************************************************",
IsCause: false,
FirstCause: false,
LastCause: false,
},
{
Number: 3,
Content: "USER_TOKEN=**********************************************************************************",
Highlighted: "USER_TOKEN=**********************************************************************************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 4,
Content: "",
Highlighted: "",
IsCause: false,
FirstCause: false,
LastCause: false,
},
},
},
Offset: 208,
}
wantFindingHuggingFace := types.SecretFinding{
RuleID: "hugging-face-access-token",
Category: secret.CategoryHuggingFace,
Title: "Hugging Face Access Token",
Severity: "CRITICAL",
StartLine: 1,
EndLine: 1,
Match: "HF_example_token: ******************************************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "HF_example_token: ******************************************",
Highlighted: "HF_example_token: ******************************************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 2,
Content: "",
Highlighted: "",
IsCause: false,
FirstCause: false,
LastCause: false,
},
},
},
Offset: 18,
}
wantFindingGrafanaQuoted := types.SecretFinding{
RuleID: "grafana-api-token",
Category: secret.CategoryGrafana,
Title: "Grafana API token",
Severity: "MEDIUM",
StartLine: 1,
EndLine: 1,
Match: "GRAFANA_TOKEN=**********************************************************************************************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "GRAFANA_TOKEN=**********************************************************************************************",
Highlighted: "GRAFANA_TOKEN=**********************************************************************************************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 2,
Content: "GRAFANA_TOKEN=**************************************************************************************",
Highlighted: "GRAFANA_TOKEN=**************************************************************************************",
IsCause: false,
FirstCause: false,
LastCause: false,
},
},
},
Offset: 14,
}
wantFindingGrafanaUnquoted := types.SecretFinding{
RuleID: "grafana-api-token",
Category: secret.CategoryGrafana,
Title: "Grafana API token",
Severity: "MEDIUM",
StartLine: 2,
EndLine: 2,
Match: "GRAFANA_TOKEN=********************************************************************************************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "GRAFANA_TOKEN=**************************************************************************************",
Highlighted: "GRAFANA_TOKEN=**************************************************************************************",
IsCause: false,
FirstCause: false,
LastCause: false,
},
{
Number: 2,
Content: "GRAFANA_TOKEN=********************************************************************************************",
Highlighted: "GRAFANA_TOKEN=********************************************************************************************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 3,
Content: "",
Highlighted: "",
},
},
},
Offset: 123,
}
wantMultiLine := types.SecretFinding{
RuleID: "multi-line-secret",
Category: "general",
Title: "Generic Rule",
Severity: "HIGH",
StartLine: 2,
EndLine: 2,
Match: "***************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "123",
Highlighted: "123",
},
{
Number: 2,
Content: "***************",
Highlighted: "***************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 3,
Content: "123",
Highlighted: "123",
},
},
},
Offset: 4,
}
wantFindingTokenInsideJs := types.SecretFinding{
RuleID: "stripe-publishable-token",
Category: "Stripe",
Title: "Stripe Publishable Key",
Severity: "LOW",
StartLine: 1,
EndLine: 1,
Match: "){case a.ez.PRODUCTION:return\"********************************\";case a.ez.TEST:cas",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "){case a.ez.PRODUCTION:return\"********************************\";case a.ez.TEST:cas",
Highlighted: "){case a.ez.PRODUCTION:return\"********************************\";case a.ez.TEST:cas",
IsCause: true,
FirstCause: true,
LastCause: true,
},
},
},
Offset: 4016,
}
wantFindingJWT := types.SecretFinding{
RuleID: "jwt-token",
Category: "JWT",
Title: "JWT token",
Severity: "MEDIUM",
StartLine: 3,
EndLine: 3,
Match: "jwt: ***********************************************************************************************************************************************************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "asd",
Highlighted: "asd",
},
{
Number: 2,
Content: "aaaa",
Highlighted: "aaaa",
},
{
Number: 3,
Content: "jwt: ***********************************************************************************************************************************************************",
Highlighted: "jwt: ***********************************************************************************************************************************************************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 4,
Content: "asda",
Highlighted: "asda",
},
},
},
Offset: 14,
}
tests := []struct {
name string
configPath string
inputFilePath string
want types.Secret
}{
{
name: "find match",
configPath: filepath.Join("testdata", "config.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "secret.txt"),
Findings: []types.SecretFinding{
wantFinding1,
wantFinding2,
},
},
},
{
name: "find aws secrets",
configPath: filepath.Join("testdata", "config.yaml"),
inputFilePath: filepath.Join("testdata", "aws-secrets.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "aws-secrets.txt"),
Findings: []types.SecretFinding{
wantFinding5,
wantFinding10,
wantFinding9,
},
},
},
{
name: "find secrets in CRLF line endings file",
configPath: filepath.Join("testdata", "skip-test.yaml"),
inputFilePath: filepath.Join("testdata", "crlf-line-endings.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "crlf-line-endings.txt"),
Findings: []types.SecretFinding{
{
RuleID: "aws-access-key-id",
Category: secret.CategoryAWS,
Title: "AWS Access Key ID",
Severity: "CRITICAL",
StartLine: 2,
EndLine: 2,
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "This is a test file with CRLF line endings.",
IsCause: false,
Highlighted: "This is a test file with CRLF line endings.",
FirstCause: false,
LastCause: false,
},
{
Number: 2,
Content: "AWS Access Key ID: ********************",
IsCause: true,
Highlighted: "AWS Access Key ID: ********************",
FirstCause: true,
LastCause: true,
},
{
Number: 3,
Content: "This line has no secrets.",
IsCause: false,
Highlighted: "This line has no secrets.",
FirstCause: false,
LastCause: false,
},
},
},
Match: "AWS Access Key ID: ********************",
Offset: 63,
},
{
RuleID: "github-pat",
Category: secret.CategoryGitHub,
Title: "GitHub Personal Access Token",
Severity: "CRITICAL",
StartLine: 4,
EndLine: 4,
Code: types.Code{
Lines: []types.Line{
{
Number: 2,
Content: "AWS Access Key ID: ********************",
IsCause: false,
Highlighted: "AWS Access Key ID: ********************",
FirstCause: false,
LastCause: false,
},
{
Number: 3,
Content: "This line has no secrets.",
IsCause: false,
Highlighted: "This line has no secrets.",
FirstCause: false,
LastCause: false,
},
{
Number: 4,
Content: "GitHub PAT: ****************************************12",
IsCause: true,
Highlighted: "GitHub PAT: ****************************************12",
FirstCause: true,
LastCause: true,
},
{
Number: 5,
Content: "End of file.",
IsCause: false,
Highlighted: "End of file.",
FirstCause: false,
LastCause: false,
},
},
},
Match: "GitHub PAT: ****************************************12",
Offset: 122,
},
},
},
},
{
name: "find Asymmetric Private Key secrets",
configPath: filepath.Join("testdata", "skip-test.yaml"),
inputFilePath: filepath.Join("testdata", "asymmetric-private-secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "asymmetric-private-secret.txt"),
Findings: []types.SecretFinding{wantFindingAsymmetricPrivateKey},
},
},
{
name: "find Alibaba AccessKey ID txt",
configPath: filepath.Join("testdata", "skip-test.yaml"),
inputFilePath: "testdata/alibaba-access-key-id.txt",
want: types.Secret{
FilePath: "testdata/alibaba-access-key-id.txt",
Findings: []types.SecretFinding{wantFindingAlibabaAccessKeyId},
},
},
{
name: "find Asymmetric Private Key secrets json",
configPath: filepath.Join("testdata", "skip-test.yaml"),
inputFilePath: filepath.Join("testdata", "asymmetric-private-secret.json"),
want: types.Secret{
FilePath: filepath.Join("testdata", "asymmetric-private-secret.json"),
Findings: []types.SecretFinding{wantFindingAsymmetricPrivateKeyJson},
},
},
{
name: "find Docker registry credentials",
configPath: filepath.Join("testdata", "skip-test.yaml"),
inputFilePath: filepath.Join("testdata", "docker-secrets.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "docker-secrets.txt"),
Findings: []types.SecretFinding{
wantFindingDockerKey1,
wantFindingDockerKey2,
},
},
},
{
name: "find Hugging face secret",
configPath: filepath.Join("testdata", "config.yaml"),
inputFilePath: filepath.Join("testdata", "hugging-face-secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "hugging-face-secret.txt"),
Findings: []types.SecretFinding{wantFindingHuggingFace},
},
},
{
name: "find grafana secret",
configPath: filepath.Join("testdata", "config.yaml"),
inputFilePath: filepath.Join("testdata", "grafana-env.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "grafana-env.txt"),
Findings: []types.SecretFinding{
wantFindingGrafanaUnquoted,
wantFindingGrafanaQuoted,
},
},
},
{
name: "find JWT token",
configPath: filepath.Join("testdata", "config.yaml"),
inputFilePath: filepath.Join("testdata", "jwt-secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "jwt-secret.txt"),
Findings: []types.SecretFinding{wantFindingJWT},
},
},
{
name: "find Private Packagist tokens",
configPath: filepath.Join("testdata", "config.yaml"),
inputFilePath: filepath.Join("testdata", "private-packagist.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "private-packagist.txt"),
Findings: []types.SecretFinding{
wantFindingPrivatePackagistOrgReadToken,
wantFindingPrivatePackagistOrgUpdateToken,
wantFindingPrivatePackagistUserToken,
},
},
},
{
name: "include when keyword found",
configPath: filepath.Join("testdata", "config-happy-keywords.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "secret.txt"),
Findings: []types.SecretFinding{
wantFinding1,
wantFinding2,
},
},
},
{
name: "exclude when no keyword found",
configPath: filepath.Join("testdata", "config-sad-keywords.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{},
},
{
name: "should ignore .md files by default",
configPath: filepath.Join("testdata", "config.yaml"),
inputFilePath: filepath.Join("testdata", "secret.md"),
want: types.Secret{
FilePath: filepath.Join("testdata", "secret.md"),
},
},
{
name: "should disable .md allow rule",
configPath: filepath.Join("testdata", "config-disable-allow-rule-md.yaml"),
inputFilePath: filepath.Join("testdata", "secret.md"),
want: types.Secret{
FilePath: filepath.Join("testdata", "secret.md"),
Findings: []types.SecretFinding{
wantFinding1,
wantFinding2,
},
},
},
{
name: "should find ghp builtin secret",
configPath: filepath.Join("testdata", "skip-test.yaml"),
inputFilePath: filepath.Join("testdata", "builtin-rule-secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "builtin-rule-secret.txt"),
Findings: []types.SecretFinding{
wantFinding5a,
wantFinding6,
},
},
},
{
name: "should find GitHub Personal Access Token (classic)",
configPath: filepath.Join("testdata", "skip-test.yaml"),
inputFilePath: "testdata/github-token.txt",
want: types.Secret{
FilePath: "testdata/github-token.txt",
Findings: []types.SecretFinding{wantFindingGitHubPAT},
},
},
{
name: "should enable github-pat builtin rule, but disable aws-access-key-id rule",
configPath: filepath.Join("testdata", "config-enable-ghp.yaml"),
inputFilePath: filepath.Join("testdata", "builtin-rule-secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "builtin-rule-secret.txt"),
Findings: []types.SecretFinding{wantFindingGHButDisableAWS},
},
},
{
name: "should disable github-pat builtin rule",
configPath: filepath.Join("testdata", "config-disable-ghp.yaml"),
inputFilePath: filepath.Join("testdata", "builtin-rule-secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "builtin-rule-secret.txt"),
Findings: []types.SecretFinding{wantFindingPATDisabled},
},
},
{
name: "should disable custom rule",
configPath: filepath.Join("testdata", "config-disable-rule1.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{},
},
{
name: "allow-rule path",
configPath: filepath.Join("testdata", "allow-path.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{},
},
{
name: "allow-rule regex inside group",
configPath: filepath.Join("testdata", "allow-regex.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "secret.txt"),
Findings: []types.SecretFinding{wantFinding1},
},
},
{
name: "allow-rule regex outside group",
configPath: filepath.Join("testdata", "allow-regex-outside-group.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{},
},
{
name: "exclude-block regexes",
configPath: filepath.Join("testdata", "exclude-block.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "secret.txt"),
Findings: []types.SecretFinding{wantFindingRegexDisabled},
},
},
{
name: "skip examples file",
configPath: filepath.Join("testdata", "skip-test.yaml"),
inputFilePath: filepath.Join("testdata", "example-secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "example-secret.txt"),
},
},
{
name: "global allow-rule path",
configPath: filepath.Join("testdata", "global-allow-path.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "secret.txt"),
Findings: nil,
},
},
{
name: "global allow-rule regex",
configPath: filepath.Join("testdata", "global-allow-regex.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "secret.txt"),
Findings: []types.SecretFinding{wantFinding1},
},
},
{
name: "global exclude-block regexes",
configPath: filepath.Join("testdata", "global-exclude-block.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "secret.txt"),
Findings: []types.SecretFinding{wantFindingRegexDisabled},
},
},
{
name: "multiple secret groups",
configPath: filepath.Join("testdata", "multiple-secret-groups.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "secret.txt"),
Findings: []types.SecretFinding{
wantFindingMultipleGroupsUsername,
wantFindingMultipleGroupsPassword,
},
},
},
{
name: "truncate long line",
configPath: filepath.Join("testdata", "skip-test.yaml"),
inputFilePath: filepath.Join("testdata", "long-line-secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "long-line-secret.txt"),
Findings: []types.SecretFinding{wantFinding7},
},
},
{
name: "add unknown severity when rule has no severity",
configPath: filepath.Join("testdata", "config-without-severity.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "secret.txt"),
Findings: []types.SecretFinding{wantFinding8},
},
},
{
name: "add unknown severity when rule has no severity",
configPath: filepath.Join("testdata", "config-with-incorrect-severity.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "secret.txt"),
Findings: []types.SecretFinding{wantFinding8},
},
},
{
name: "update severity if rule severity is not in uppercase",
configPath: filepath.Join("testdata", "config-with-non-uppercase-severity.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "secret.txt"),
Findings: []types.SecretFinding{wantFinding8},
},
},
{
name: "use unknown severity when rule has incorrect severity",
configPath: filepath.Join("testdata", "config-with-incorrect-severity.yaml"),
inputFilePath: filepath.Join("testdata", "secret.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "secret.txt"),
Findings: []types.SecretFinding{wantFinding8},
},
},
{
name: "invalid aws secrets",
configPath: filepath.Join("testdata", "skip-test.yaml"),
inputFilePath: filepath.Join("testdata", "invalid-aws-secrets.txt"),
want: types.Secret{},
},
{
name: "secret inside another word",
configPath: filepath.Join("testdata", "skip-test.yaml"),
inputFilePath: filepath.Join("testdata", "wrapped-secrets.txt"),
want: types.Secret{},
},
{
name: "sensitive secret inside another word",
configPath: filepath.Join("testdata", "skip-test.yaml"),
inputFilePath: filepath.Join("testdata", "wrapped-secrets-sensitive.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "wrapped-secrets-sensitive.txt"),
Findings: []types.SecretFinding{
wantFindingMyAwsAccessKey,
wantFindingMyGitHubPAT,
},
},
},
{
name: "asymmetric file",
configPath: filepath.Join("testdata", "skip-test.yaml"),
inputFilePath: "testdata/asymmetric-private-key.txt",
want: types.Secret{
FilePath: "testdata/asymmetric-private-key.txt",
Findings: []types.SecretFinding{
wantFindingAsymmSecretKey,
wantFindingMinimumAsymmSecretKey,
},
},
},
{
name: "begin/end line symbols without multi-line mode",
configPath: filepath.Join("testdata", "multi-line-off.yaml"),
inputFilePath: "testdata/multi-line.txt",
want: types.Secret{},
},
{
name: "begin/end line symbols with multi-line mode",
configPath: filepath.Join("testdata", "multi-line-on.yaml"),
inputFilePath: "testdata/multi-line.txt",
want: types.Secret{
FilePath: "testdata/multi-line.txt",
Findings: []types.SecretFinding{wantMultiLine},
},
},
{
name: "long obfuscated js code with secrets",
configPath: filepath.Join("testdata", "skip-test.yaml"),
inputFilePath: filepath.Join("testdata", "obfuscated.js"),
want: types.Secret{
FilePath: filepath.Join("testdata", "obfuscated.js"),
Findings: []types.SecretFinding{wantFindingTokenInsideJs},
},
},
{
name: "invalid UTF-8 sequences in secrets",
configPath: filepath.Join("testdata", "skip-test.yaml"),
inputFilePath: filepath.Join("testdata", "invalid-utf8.txt"),
want: types.Secret{
FilePath: filepath.Join("testdata", "invalid-utf8.txt"),
Findings: []types.SecretFinding{
{
RuleID: "github-pat",
Category: secret.CategoryGitHub,
Title: "GitHub Personal Access Token",
Severity: "CRITICAL",
StartLine: 1,
EndLine: 1,
Match: "token=****************************************",
Code: types.Code{
Lines: []types.Line{
{
Number: 1,
Content: "token=****************************************",
Highlighted: "token=****************************************",
IsCause: true,
FirstCause: true,
LastCause: true,
},
{
Number: 2,
Content: "# Comment with invalid UTF-8: <20>",
Highlighted: "# Comment with invalid UTF-8: <20>",
},
},
},
Offset: 6,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
content, err := os.ReadFile(tt.inputFilePath)
require.NoError(t, err)
content = bytes.ReplaceAll(content, []byte("\r"), []byte(""))
c, err := secret.ParseConfig(tt.configPath)
require.NoError(t, err)
s := secret.NewScanner(c)
got := s.Scan(secret.ScanArgs{
FilePath: tt.inputFilePath,
Content: bytes.NewReader(content),
})
assert.Equal(t, tt.want, got)
})
}
}
func TestSecretScannerWithStreaming(t *testing.T) {
tests := []struct {
name string
input string
bufferSize int
overlapSize int
configPath string
want types.Secret
}{
{
name: "secret in second chunk",
input: strings.Repeat("x", 520) + "\n" + // 520 bytes to push secret to second chunk
"AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF\n" + // at offset 521
strings.Repeat("y", 100), // padding
bufferSize: 512,
overlapSize: 128,
configPath: filepath.Join("testdata", "skip-test.yaml"),
want: types.Secret{
FilePath: "test.txt",
Findings: []types.SecretFinding{
{
RuleID: "aws-access-key-id",
Category: secret.CategoryAWS,
Title: "AWS Access Key ID",
Severity: "CRITICAL",
StartLine: 2,
EndLine: 2,
Match: "AWS_ACCESS_KEY_ID=********************",
Offset: 539, // 521 + 18 (position of the actual key value)
},
},
},
},
{
name: "secret spanning chunk boundary",
input: strings.Repeat("x", 480) + "\n" + // 480 bytes
"AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF\n" + // at offset 481, spans chunk boundary at 512
strings.Repeat("y", 200), // padding
bufferSize: 512, // Boundary will be in the middle of the secret
overlapSize: 128,
configPath: filepath.Join("testdata", "skip-test.yaml"),
want: types.Secret{
FilePath: "test.txt",
Findings: []types.SecretFinding{
{
RuleID: "aws-access-key-id",
Category: secret.CategoryAWS,
Title: "AWS Access Key ID",
Severity: "CRITICAL",
StartLine: 2,
EndLine: 2,
Match: "AWS_ACCESS_KEY_ID=********************",
Offset: 499, // 481 + 18
},
},
},
},
{
name: "multiple secrets across chunks",
input: "GITHUB_PAT=ghp_012345678901234567890123456789abcdef\n" + // at offset 0, 52 bytes
strings.Repeat("x", 1200) + "\n" + // 1200 bytes padding
"AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF\n" + // at offset 1253
strings.Repeat("y", 1400) + "\n" + // 1400 bytes padding
"stripe_key=sk_test_51H5Z3jGXvP5CVwYOLLllllllllllllllllllllllll\n" + // at offset 2693
strings.Repeat("z", 200), // final padding
bufferSize: 1024,
overlapSize: 256,
configPath: filepath.Join("testdata", "skip-test.yaml"),
want: types.Secret{
FilePath: "test.txt",
Findings: []types.SecretFinding{
{
RuleID: "aws-access-key-id",
Category: secret.CategoryAWS,
Title: "AWS Access Key ID",
Severity: "CRITICAL",
StartLine: 3,
EndLine: 3,
Match: "AWS_ACCESS_KEY_ID=********************",
Offset: 1271, // 1253 + 18
},
{
RuleID: "github-pat",
Category: secret.CategoryGitHub,
Title: "GitHub Personal Access Token",
Severity: "CRITICAL",
StartLine: 1,
EndLine: 1,
Match: "GITHUB_PAT=****************************************", // 40 asterisks for 40-char token
Offset: 11, // position of the token value
},
{
RuleID: "stripe-secret-token",
Category: secret.CategoryStripe,
Title: "Stripe Secret Key",
Severity: "CRITICAL",
StartLine: 5,
EndLine: 5,
Match: "stripe_key=****************************************lllllllllll", // Stripe key pattern includes literal 'l' chars
Offset: 2704, // 2693 + 11
},
},
},
},
{
name: "secret at exact chunk boundary",
input: strings.Repeat("x", 383) + "\n" + // 383 bytes + newline = 384
"AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF", // Starts exactly at chunk boundary (384)
bufferSize: 384, // Boundary right after the newline
overlapSize: 96,
configPath: filepath.Join("testdata", "skip-test.yaml"),
want: types.Secret{
FilePath: "test.txt",
Findings: []types.SecretFinding{
{
RuleID: "aws-access-key-id",
Category: secret.CategoryAWS,
Title: "AWS Access Key ID",
Severity: "CRITICAL",
StartLine: 2,
EndLine: 2,
Match: "AWS_ACCESS_KEY_ID=********************",
Offset: 402, // 384 + 18
},
},
},
},
{
name: "very small buffer and overlap",
input: strings.Repeat("x", 150) + "\n" + // 150 bytes padding
"secret=\"mysecret123\"\n" + // at offset 151
strings.Repeat("y", 345) + "\n" + // 345 bytes padding
"secret=\"anothersecret456\"\n" + // at offset 517
strings.Repeat("z", 150), // suffix padding
bufferSize: 128,
overlapSize: 32,
configPath: filepath.Join("testdata", "config.yaml"),
want: types.Secret{
FilePath: "test.txt",
Findings: []types.SecretFinding{
{
RuleID: "rule1",
Category: "general",
Title: "Generic Rule",
Severity: "HIGH",
StartLine: 2,
EndLine: 2,
Match: "secret=\"***********\"",
Offset: 159, // 151 + 8 (position of "mysecret123")
},
{
RuleID: "rule1",
Category: "general",
Title: "Generic Rule",
Severity: "HIGH",
StartLine: 4,
EndLine: 4,
Match: "secret=\"****************\"",
Offset: 526, // 517 + 9
},
},
},
},
{
name: "multi-line secret with small chunks",
input: strings.Repeat("x", 720) + "\n" + // 720 bytes padding
"-----BEGIN RSA PRIVATE KEY-----\n" + // at offset 721
"MIIEpAIBAAKCAQEA1234567890abcdefghijklmnopqrstuvwxyz\n" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnop\n" +
"qrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12345678\n" +
"-----END RSA PRIVATE KEY-----\n" +
strings.Repeat("z", 460), // suffix padding
bufferSize: 1024, // Ensure the entire key fits in one chunk
overlapSize: 256,
configPath: filepath.Join("testdata", "skip-test.yaml"),
want: types.Secret{
FilePath: "test.txt",
Findings: []types.SecretFinding{
{
RuleID: "private-key",
Category: secret.CategoryAsymmetricPrivateKey,
Title: "Asymmetric Private Key",
Severity: "HIGH",
StartLine: 3,
EndLine: 5,
Match: "****************************************************", // Multi-line secret content
Offset: 753, // 721 + 32 (after BEGIN line)
},
},
},
},
{
name: "multi-line secret exceeding overlap size (known limitation)",
input: strings.Repeat("x", 920) + "\n" + // Position key at chunk boundary (1024 - ~100 bytes for key)
"-----BEGIN RSA PRIVATE KEY-----\n" + // at offset 921
"MIIEpAIBAAKCAQEA1234567890abcdefghijklmnopqrstuvwxyz\n" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnop\n" +
"qrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12345678\n" +
"-----END RSA PRIVATE KEY-----\n" +
strings.Repeat("z", 460), // suffix padding
bufferSize: 1024,
overlapSize: 100, // Too small to capture the entire key
configPath: filepath.Join("testdata", "skip-test.yaml"),
want: types.Secret{
FilePath: "test.txt",
Findings: nil, // Key won't be detected as it spans beyond the overlap
},
},
{
name: "no secrets in any chunk",
input: strings.Repeat("this is just normal content without any secrets in it at all\n", 50),
bufferSize: 512,
overlapSize: 128, // 1/4 of buffer
configPath: filepath.Join("testdata", "config.yaml"),
want: types.Secret{
FilePath: "test.txt",
Findings: nil,
},
},
{
name: "secret in last chunk with EOF",
input: strings.Repeat("x", 1500) + "\n" + // 1500 bytes to push secret to end
"final_secret=\"supersecretvalue\"", // at offset 1501, no newline at end
bufferSize: 640,
overlapSize: 160, // 1/4 of buffer
configPath: filepath.Join("testdata", "config.yaml"),
want: types.Secret{
FilePath: "test.txt",
Findings: []types.SecretFinding{
{
RuleID: "rule1",
Category: "general",
Title: "Generic Rule",
Severity: "HIGH",
StartLine: 2,
EndLine: 2,
Match: "final_secret=\"****************\"",
Offset: 1515, // 1501 + 14 (position of "supersecretvalue")
},
},
},
},
{
name: "deduplicate findings at chunk boundaries",
input: strings.Repeat("x", 330) + "\n" + // 330 bytes
"secret=\"duplicatetest123\"\n" + // at offset 331, ends at 356
strings.Repeat("y", 200), // padding
bufferSize: 400, // First chunk: 0-399, overlap: 300-399
overlapSize: 100, // Secret (331-356) is within overlap region
configPath: filepath.Join("testdata", "config.yaml"),
want: types.Secret{
FilePath: "test.txt",
Findings: []types.SecretFinding{
{
RuleID: "rule1",
Category: "general",
Title: "Generic Rule",
Severity: "HIGH",
StartLine: 2,
EndLine: 2,
Match: "secret=\"****************\"",
Offset: 339, // 331 + 8 (position of "duplicatetest123")
},
},
},
},
{
name: "multiple secrets on same line",
input: strings.Repeat("x", 480) + "\n" + // 480 bytes padding
"secret=\"first123\" and secret=\"second456\" on same line\n" + // at offset 481
strings.Repeat("y", 1300), // large padding to force multiple chunks
bufferSize: 512,
overlapSize: 128, // 1/4 of buffer
configPath: filepath.Join("testdata", "config.yaml"),
want: types.Secret{
FilePath: "test.txt",
Findings: []types.SecretFinding{
{
RuleID: "rule1",
Category: "general",
Title: "Generic Rule",
Severity: "HIGH",
StartLine: 2,
EndLine: 2,
Match: "secret=\"********\" and secret=\"*********\" on same line", // Full line match for second secret
Offset: 511, // 481 + 30 (position of "second456")
},
{
RuleID: "rule1",
Category: "general",
Title: "Generic Rule",
Severity: "HIGH",
StartLine: 2,
EndLine: 2,
Match: "secret=\"********\" and secret=\"s", // Truncated match for first secret
Offset: 489, // 481 + 8 (position of "first123")
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Parse config
c, err := secret.ParseConfig(tt.configPath)
require.NoError(t, err)
// Create scanner with custom buffer and overlap sizes
s := secret.NewScanner(c,
secret.WithBufferSize(tt.bufferSize),
secret.WithOverlapSize(tt.overlapSize))
// Scan with streaming
reader := strings.NewReader(tt.input)
got := s.Scan(secret.ScanArgs{
FilePath: "test.txt",
Content: reader,
})
// Clear Code field as it's too verbose to specify in test expectations
for i := range got.Findings {
got.Findings[i].Code = types.Code{}
}
for i := range tt.want.Findings {
tt.want.Findings[i].Code = types.Code{}
}
// Compare all findings at once
assert.Equal(t, tt.want.Findings, got.Findings, "unexpected findings")
})
}
}