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
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# and "Missing User Instruction" since 2ms container is stopped after scan

# Builder image
FROM checkmarx/go:1.27.0-r1-7ff3a27a305109@sha256:7ff3a27a305109341ebf351a1421172d7ee41aeeeb0609451ddb6c8ee5d144b3 AS builder
FROM checkmarx/go:1.27.1-r0-424cf19b9e848d@sha256:424cf19b9e848d86bbf0ed45b216d782f064bfb6b1dd7eba7f5a8cc3f750088f AS builder

WORKDIR /app

Expand All @@ -20,7 +20,7 @@ COPY . .
RUN GOOS=linux GOARCH=amd64 go build -buildvcs=false -ldflags="-s -w" -a -o /app/2ms .

# Runtime image
FROM checkmarx/git:2.55.0-r5-d0ccbb0b82fcb8@sha256:d0ccbb0b82fcb8c84ee36087b47eefbf59f8259c4f902fbb3591acd1ee00c546
FROM checkmarx/git:2.55.0-r7-193d1e713216b7@sha256:193d1e713216b75b63eb05c3ebac0185620565b10a33d2ca1b3a89e8bd46c4fc

WORKDIR /app

Expand Down
36 changes: 35 additions & 1 deletion engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,39 @@ func GetRulesCommand(engineConfig *EngineConfig) *cobra.Command {
}
}

// secretSuffixTailRegexes match, at the end of a string, whatever a rule's secret-suffix
// regex would have matched right after the secret's capture group. They're derived directly
// from ruledefine.SecretSuffix and ruledefine.SecretSuffixIncludingXml
var secretSuffixTailRegexes = []*regexp.Regexp{
regexp.MustCompile(strings.TrimPrefix(ruledefine.SecretSuffix, ")") + "$"),
regexp.MustCompile(strings.TrimPrefix(ruledefine.SecretSuffixIncludingXml, ")") + "$"),
}

// trimSecretSuffixOverlap returns endColumn adjusted so it no longer includes the trailing
// boundary characters matched by the rule's secret-suffix regex.
//
// Both suffix regexes always have a zero-width `$` alternative, so they'll always "match" at
// endColumn itself; what matters is the longest overlap found across both regexes, not merely
// whether one of them matched.
func trimSecretSuffixOverlap(line string, endColumn int) int {
if endColumn <= 0 || endColumn > len(line) {
return endColumn
}
head := line[:endColumn]

overlap := 0
for _, re := range secretSuffixTailRegexes {
matches := re.FindStringIndex(head)
if matches != nil {
matchedSuffixLength := matches[1] - matches[0]
if matchedSuffixLength > overlap {
overlap = matchedSuffixLength
}
}
}
return endColumn - overlap
}

// buildSecret creates a secret object from the given source item and finding
func buildSecret(
ctx context.Context,
Expand All @@ -547,13 +580,14 @@ func buildSecret(

hasNewline := strings.HasPrefix(value.Line, "\n")

adjustedEndColumn := trimSecretSuffixOverlap(value.Line, value.EndColumn)

if hasNewline {
value.Line = strings.TrimPrefix(value.Line, "\n")
}
value.Line = strings.ReplaceAll(value.Line, "\r", "")

adjustedStartColumn := value.StartColumn
adjustedEndColumn := value.EndColumn
if hasNewline {
adjustedStartColumn--
adjustedEndColumn--
Expand Down
77 changes: 76 additions & 1 deletion engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -476,9 +476,19 @@ func TestDetectChunks(t *testing.T) {

func TestSecretsColumnIndex(t *testing.T) {

const defaultSecret = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"

xmlSuffixSecret := "AIzaSyATD"
xmlSuffixLine := "<string>" + xmlSuffixSecret + "</string>"
xmlSuffixSecretStart := len("<string>") + 1
xmlSuffixSecretEnd := xmlSuffixSecretStart + len(xmlSuffixSecret) - 1

generalSuffixSecret := "5qnwhuk"

tests := []struct {
name string
lineContent string
secret string
startColumn int
endColumn int
expectedLineContent string
Expand All @@ -488,6 +498,7 @@ func TestSecretsColumnIndex(t *testing.T) {
{
name: "secret on first line without newline",
lineContent: `let apikey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"`,
secret: defaultSecret,
startColumn: 14,
endColumn: 50,
expectedLineContent: `let apikey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"`,
Expand All @@ -497,6 +508,7 @@ func TestSecretsColumnIndex(t *testing.T) {
{
name: "secret with leading newline",
lineContent: "\nlet apikey = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\"",
secret: defaultSecret,
startColumn: 15,
endColumn: 51,
expectedLineContent: `let apikey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"`,
Expand All @@ -506,6 +518,7 @@ func TestSecretsColumnIndex(t *testing.T) {
{
name: "leading newline followed by tab indentation",
lineContent: "\n let apikey = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\"",
secret: defaultSecret,
startColumn: 2,
endColumn: 7,
expectedLineContent: " let apikey = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\"",
Expand All @@ -515,6 +528,7 @@ func TestSecretsColumnIndex(t *testing.T) {
{
name: "leading newline followed by tab indentation with special character",
lineContent: "\n\tlet apikey€ = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\"",
secret: defaultSecret,
startColumn: 2,
endColumn: 7,
expectedLineContent: " let apikey€ = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\"",
Expand All @@ -524,12 +538,73 @@ func TestSecretsColumnIndex(t *testing.T) {
{
name: "newline with content larger than context limit",
lineContent: "\n" + strings.Repeat("A", 500) + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + strings.Repeat("B", 500),
secret: defaultSecret,
startColumn: 501,
endColumn: 536,
expectedLineContent: strings.Repeat("A", 250) + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + strings.Repeat("B", 250),
expectedStartColumn: 500,
expectedEndColumn: 535,
},
{
name: "generic-api-key xml suffix consumes closing tag",
lineContent: xmlSuffixLine,
secret: xmlSuffixSecret,
startColumn: xmlSuffixSecretStart,
endColumn: xmlSuffixSecretEnd + len("</string>"),
expectedLineContent: xmlSuffixLine,
expectedStartColumn: xmlSuffixSecretStart,
expectedEndColumn: xmlSuffixSecretEnd,
},
{
name: "secret followed by carriage return",
lineContent: generalSuffixSecret + "\r",
secret: generalSuffixSecret,
startColumn: 1,
endColumn: len(generalSuffixSecret + "\r"),
expectedLineContent: generalSuffixSecret, // buildSecret strips all \r bytes from Line
expectedStartColumn: 1,
expectedEndColumn: len(generalSuffixSecret),
},
{
name: "secret followed by newline",
lineContent: generalSuffixSecret + "\n",
secret: generalSuffixSecret,
startColumn: 1,
endColumn: len(generalSuffixSecret + "\n"),
expectedLineContent: generalSuffixSecret + "\n",
expectedStartColumn: 1,
expectedEndColumn: len(generalSuffixSecret),
},
{
name: "secret followed by semicolon",
lineContent: generalSuffixSecret + ";",
secret: generalSuffixSecret,
startColumn: 1,
endColumn: len(generalSuffixSecret + ";"),
expectedLineContent: generalSuffixSecret + ";",
expectedStartColumn: 1,
expectedEndColumn: len(generalSuffixSecret),
},
{
name: `secret followed by double quote`,
lineContent: generalSuffixSecret + `"`,
secret: generalSuffixSecret,
startColumn: 1,
endColumn: len(generalSuffixSecret + `"`),
expectedLineContent: generalSuffixSecret + `"`,
expectedStartColumn: 1,
expectedEndColumn: len(generalSuffixSecret),
},
{
name: `secret followed by literal backslash-r backslash-n`,
lineContent: generalSuffixSecret + `\r\n`,
secret: generalSuffixSecret,
startColumn: 1,
endColumn: len(generalSuffixSecret + `\r\n`),
expectedLineContent: generalSuffixSecret + `\r\n`,
expectedStartColumn: 1,
expectedEndColumn: len(generalSuffixSecret + `\r`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -539,7 +614,7 @@ func TestSecretsColumnIndex(t *testing.T) {
finding := report.Finding{
StartColumn: tt.startColumn,
EndColumn: tt.endColumn,
Secret: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
Secret: tt.secret,
RuleID: "test-rule",
Description: "Test Description",
Line: tt.lineContent,
Expand Down
4 changes: 2 additions & 2 deletions engine/rules/ruledefine/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const (
secretPrefixUnique = `\b(`
secretPrefix = `[\x60'"\s=]{0,20}(` //nolint:gosec // This is a regex pattern
SecretSuffix = `)(?:[\x60'"\s;]|\\[nr]|$)` //nolint:gosec // This is a regex pattern
secretSuffixIncludingXml = `)(?:['|\"|\n|\r|\s|\x60|;]|\\n|\\r|$|\s{0,10}<\/string>)` //nolint:gosec // This is a regex pattern
SecretSuffixIncludingXml = `)(?:['|\"|\n|\r|\s|\x60|;]|\\n|\\r|$|\s{0,10}<\/string>)` //nolint:gosec // This is a regex pattern
)

func generateSemiGenericRegex(identifiers []string, secretRegex string, isCaseInsensitive bool) *regexp.Regexp {
Expand Down Expand Up @@ -83,7 +83,7 @@ func generateSemiGenericRegexIncludingXml(identifiers []string, secretRegex stri
sb.WriteString(operator)
sb.WriteString(secretPrefix)
sb.WriteString(secretRegex)
sb.WriteString(secretSuffixIncludingXml)
sb.WriteString(SecretSuffixIncludingXml)
return regexp.MustCompile(sb.String())
}

Expand Down
Loading