diff --git a/Dockerfile b/Dockerfile
index 399da46e..9211c54d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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
@@ -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
diff --git a/engine/engine.go b/engine/engine.go
index 9f696971..6c2e330b 100644
--- a/engine/engine.go
+++ b/engine/engine.go
@@ -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,
@@ -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--
diff --git a/engine/engine_test.go b/engine/engine_test.go
index 0fc7cabb..30e3ed49 100644
--- a/engine/engine_test.go
+++ b/engine/engine_test.go
@@ -476,9 +476,19 @@ func TestDetectChunks(t *testing.T) {
func TestSecretsColumnIndex(t *testing.T) {
+ const defaultSecret = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
+
+ xmlSuffixSecret := "AIzaSyATD"
+ xmlSuffixLine := "" + xmlSuffixSecret + ""
+ xmlSuffixSecretStart := len("") + 1
+ xmlSuffixSecretEnd := xmlSuffixSecretStart + len(xmlSuffixSecret) - 1
+
+ generalSuffixSecret := "5qnwhuk"
+
tests := []struct {
name string
lineContent string
+ secret string
startColumn int
endColumn int
expectedLineContent string
@@ -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"`,
@@ -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"`,
@@ -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\"",
@@ -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\"",
@@ -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(""),
+ 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) {
@@ -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,
diff --git a/engine/rules/ruledefine/utils.go b/engine/rules/ruledefine/utils.go
index 529b628a..4993317a 100644
--- a/engine/rules/ruledefine/utils.go
+++ b/engine/rules/ruledefine/utils.go
@@ -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 {
@@ -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())
}