From 161663aa58d37c47d6933b900be8b0cd7b463c8b Mon Sep 17 00:00:00 2001
From: Diogo Rocha <104084969+cx-diogo-rocha@users.noreply.github.com>
Date: Wed, 2 Sep 2026 18:27:26 +0100
Subject: [PATCH 01/12] Fix line content, now correctly handling repeated
secrets in same line
---
engine/config.go | 1 +
engine/engine.go | 10 +--
engine/linecontent/linecontent.go | 16 ++++-
engine/linecontent/linecontent_test.go | 84 +++++++++++++++++---------
4 files changed, 74 insertions(+), 37 deletions(-)
diff --git a/engine/config.go b/engine/config.go
index 4af1444a..2b0c630f 100644
--- a/engine/config.go
+++ b/engine/config.go
@@ -37,6 +37,7 @@ var baseConfig = config.Config{
regexp.MustCompile(`verification-metadata\.xml`),
regexp.MustCompile(`Database.refactorlog`),
regexp.MustCompile(`(?:^|/)\.git$`),
+ regexp.MustCompile(`(?:^|/)secret\.doc$`),
},
},
},
diff --git a/engine/engine.go b/engine/engine.go
index d99a65a8..9f696971 100644
--- a/engine/engine.go
+++ b/engine/engine.go
@@ -552,11 +552,6 @@ func buildSecret(
}
value.Line = strings.ReplaceAll(value.Line, "\r", "")
- lineContent, err := linecontent.GetLineContent(value.Line, value.Secret)
- if err != nil {
- return nil, fmt.Errorf("failed to get line content for source %s: %w", item.GetSource(), err)
- }
-
adjustedStartColumn := value.StartColumn
adjustedEndColumn := value.EndColumn
if hasNewline {
@@ -564,6 +559,11 @@ func buildSecret(
adjustedEndColumn--
}
+ lineContent, err := linecontent.GetLineContent(value.Line, value.Secret, adjustedStartColumn)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get line content for source %s: %w", item.GetSource(), err)
+ }
+
secret := &secrets.Secret{
ID: findingID,
Source: item.GetSource(),
diff --git a/engine/linecontent/linecontent.go b/engine/linecontent/linecontent.go
index 5cc20c97..23713484 100644
--- a/engine/linecontent/linecontent.go
+++ b/engine/linecontent/linecontent.go
@@ -11,7 +11,7 @@ const (
contextRightSizeLimit = 250
)
-func GetLineContent(line, secret string) (string, error) {
+func GetLineContent(line, secret string, startColumn int) (string, error) {
lineSize := len(line)
if lineSize == 0 {
return "", fmt.Errorf("line empty")
@@ -27,8 +27,18 @@ func GetLineContent(line, secret string) (string, error) {
lineSize = lineMaxParseSize
}
- // Find the secret's position in the line
- secretStartIndex := strings.Index(line, secret)
+ // Find the secret's position in the line, searching from the match's start column
+ // onwards first so that repeated occurrences of secret in the line are disambiguated.
+ secretStartIndex := -1
+ hint := startColumn - 1
+ if hint >= 0 && hint < lineSize {
+ if idx := strings.Index(line[hint:], secret); idx != -1 {
+ secretStartIndex = hint + idx
+ }
+ }
+ if secretStartIndex == -1 {
+ secretStartIndex = strings.Index(line, secret)
+ }
if secretStartIndex == -1 {
// Secret not found, return truncated content based on context limits
maxSize := contextLeftSizeLimit + contextRightSizeLimit
diff --git a/engine/linecontent/linecontent_test.go b/engine/linecontent/linecontent_test.go
index ba81549e..ad5d31bb 100644
--- a/engine/linecontent/linecontent_test.go
+++ b/engine/linecontent/linecontent_test.go
@@ -14,6 +14,7 @@ func TestGetLineContent(t *testing.T) {
name string
line string
secret string
+ startColumn int
expected string
error bool
errorMessage string
@@ -59,11 +60,12 @@ func TestGetLineContent(t *testing.T) {
error: false,
},
{
- name: "Secret at the beginning with line size smaller than the parse limit",
- line: "start:" + dummySecret + strings.Repeat("A", lineMaxParseSize/2),
- secret: dummySecret,
- expected: "start:" + dummySecret + strings.Repeat("A", contextRightSizeLimit),
- error: false,
+ name: "Secret at the beginning with line size smaller than the parse limit",
+ line: "start:" + dummySecret + strings.Repeat("A", lineMaxParseSize/2),
+ secret: dummySecret,
+ startColumn: len("start:") + 1,
+ expected: "start:" + dummySecret + strings.Repeat("A", contextRightSizeLimit),
+ error: false,
},
{
name: "Secret found in middle with line size smaller than the parse limit",
@@ -74,43 +76,67 @@ func TestGetLineContent(t *testing.T) {
"A",
contextRightSizeLimit,
) + "end",
- secret: dummySecret,
- expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", contextRightSizeLimit),
- error: false,
+ secret: dummySecret,
+ startColumn: len("start") + contextLeftSizeLimit + 1,
+ expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", contextRightSizeLimit),
+ error: false,
},
{
- name: "Secret at the end with line size smaller than the parse limit",
- line: strings.Repeat("A", lineMaxParseSize/2) + dummySecret + ":end",
- secret: dummySecret,
- expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + ":end",
- error: false,
+ name: "Secret at the end with line size smaller than the parse limit",
+ line: strings.Repeat("A", lineMaxParseSize/2) + dummySecret + ":end",
+ secret: dummySecret,
+ startColumn: lineMaxParseSize/2 + 1,
+ expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + ":end",
+ error: false,
},
{
- name: "Secret at the beginning with line size larger than the parse limit",
- line: "start:" + dummySecret + strings.Repeat("A", lineMaxParseSize),
- secret: dummySecret,
- expected: "start:" + dummySecret + strings.Repeat("A", contextRightSizeLimit),
- error: false,
+ name: "Secret at the beginning with line size larger than the parse limit",
+ line: "start:" + dummySecret + strings.Repeat("A", lineMaxParseSize),
+ secret: dummySecret,
+ startColumn: len("start:") + 1,
+ expected: "start:" + dummySecret + strings.Repeat("A", contextRightSizeLimit),
+ error: false,
},
{
- name: "Secret found in middle with line size larger than the parse limit",
- line: "start" + strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", lineMaxParseSize) + "end",
- secret: dummySecret,
- expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", contextRightSizeLimit),
- error: false,
+ name: "Secret found in middle with line size larger than the parse limit",
+ line: "start" + strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", lineMaxParseSize) + "end",
+ secret: dummySecret,
+ startColumn: len("start") + contextLeftSizeLimit + 1,
+ expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", contextRightSizeLimit),
+ error: false,
},
{
- name: "Secret at the end with line size larger than the parse limit",
- line: strings.Repeat("A", lineMaxParseSize-100) + dummySecret + strings.Repeat("A", lineMaxParseSize),
- secret: dummySecret,
- expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", 100-len(dummySecret)),
- error: false,
+ name: "Secret at the end with line size larger than the parse limit",
+ line: strings.Repeat("A", lineMaxParseSize-100) + dummySecret + strings.Repeat("A", lineMaxParseSize),
+ secret: dummySecret,
+ startColumn: lineMaxParseSize - 100 + 1,
+ expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", 100-len(dummySecret)),
+ error: false,
+ },
+ {
+ name: "Secret repeated on the same line uses the occurrence at startColumn",
+ line: strings.Repeat("a", contextLeftSizeLimit) + dummySecret + strings.Repeat("b", contextLeftSizeLimit+contextRightSizeLimit) + dummySecret + strings.Repeat("c", contextRightSizeLimit),
+ secret: dummySecret,
+ startColumn: contextLeftSizeLimit + len(dummySecret) + contextLeftSizeLimit + contextRightSizeLimit + 1,
+ expected: strings.Repeat("b", contextLeftSizeLimit) + dummySecret + strings.Repeat("c", contextRightSizeLimit),
+ error: false,
+ },
+ {
+ name: "Secret repeated with startColumn pointing at the containing match, not the secret itself",
+ line: strings.Repeat("a", contextLeftSizeLimit) + "KEY=" + dummySecret + strings.Repeat(
+ "b",
+ contextLeftSizeLimit+contextRightSizeLimit,
+ ) + "KEY=" + dummySecret + strings.Repeat("c", contextRightSizeLimit),
+ secret: dummySecret,
+ startColumn: contextLeftSizeLimit + len("KEY=") + len(dummySecret) + contextLeftSizeLimit + contextRightSizeLimit + 1,
+ expected: strings.Repeat("b", contextLeftSizeLimit-len("KEY=")) + "KEY=" + dummySecret + strings.Repeat("c", contextRightSizeLimit),
+ error: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- got, err := GetLineContent(tt.line, tt.secret)
+ got, err := GetLineContent(tt.line, tt.secret, tt.startColumn)
if (err != nil) != tt.error {
t.Fatalf("GetLineContent() error = %v, wantErr %v", err, tt.error)
}
From 817f44a6fb53defa43ee3974ab6c130e3a76b234 Mon Sep 17 00:00:00 2001
From: Diogo Rocha <104084969+cx-diogo-rocha@users.noreply.github.com>
Date: Thu, 3 Sep 2026 12:22:13 +0100
Subject: [PATCH 02/12] Adding support for line contents bigger than 10K bytes
---
engine/linecontent/linecontent.go | 11 +++++++++--
engine/linecontent/linecontent_test.go | 16 +++++++++++++++-
2 files changed, 24 insertions(+), 3 deletions(-)
diff --git a/engine/linecontent/linecontent.go b/engine/linecontent/linecontent.go
index 23713484..db816bd4 100644
--- a/engine/linecontent/linecontent.go
+++ b/engine/linecontent/linecontent.go
@@ -21,10 +21,17 @@ func GetLineContent(line, secret string, startColumn int) (string, error) {
return "", fmt.Errorf("secret empty")
}
- // Truncate lineContent to max size
+ // Truncate lineContent to max size, centering the window on startColumn so a secret
+ // far into a very long line isn't truncated away before it can be found.
if lineSize > lineMaxParseSize {
- line = line[:lineMaxParseSize]
+ windowStart := 0
+ if hint := startColumn - 1; hint >= 0 && hint < lineSize {
+ windowStart = max(hint-lineMaxParseSize/2, 0)
+ windowStart = min(windowStart, lineSize-lineMaxParseSize)
+ }
+ line = line[windowStart : windowStart+lineMaxParseSize]
lineSize = lineMaxParseSize
+ startColumn -= windowStart
}
// Find the secret's position in the line, searching from the match's start column
diff --git a/engine/linecontent/linecontent_test.go b/engine/linecontent/linecontent_test.go
index ad5d31bb..f7a2fa08 100644
--- a/engine/linecontent/linecontent_test.go
+++ b/engine/linecontent/linecontent_test.go
@@ -110,7 +110,7 @@ func TestGetLineContent(t *testing.T) {
line: strings.Repeat("A", lineMaxParseSize-100) + dummySecret + strings.Repeat("A", lineMaxParseSize),
secret: dummySecret,
startColumn: lineMaxParseSize - 100 + 1,
- expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", 100-len(dummySecret)),
+ expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", contextRightSizeLimit),
error: false,
},
{
@@ -132,6 +132,20 @@ func TestGetLineContent(t *testing.T) {
expected: strings.Repeat("b", contextLeftSizeLimit-len("KEY=")) + "KEY=" + dummySecret + strings.Repeat("c", contextRightSizeLimit),
error: false,
},
+ {
+ name: "Secret far beyond the parse limit is not truncated away",
+ line: strings.Repeat("x", lineMaxParseSize*3) + "PRE" + dummySecret + "POST" + strings.Repeat(
+ "y",
+ lineMaxParseSize,
+ ),
+ secret: dummySecret,
+ startColumn: lineMaxParseSize*3 + len("PRE") + 1,
+ expected: strings.Repeat("x", contextLeftSizeLimit-len("PRE")) + "PRE" + dummySecret + "POST" + strings.Repeat(
+ "y",
+ contextRightSizeLimit-len("POST"),
+ ),
+ error: false,
+ },
}
for _, tt := range tests {
From 876541e1f5d9aea74757fd384deaa4d4d964624d Mon Sep 17 00:00:00 2001
From: Diogo Rocha <104084969+cx-diogo-rocha@users.noreply.github.com>
Date: Thu, 3 Sep 2026 17:35:18 +0100
Subject: [PATCH 03/12] Another fix to line content. Exporting RegexSuffix
---
engine/linecontent/linecontent.go | 23 ++++++++++-------------
engine/rules/ruledefine/utils.go | 6 +++---
pkg/rules.go | 2 ++
3 files changed, 15 insertions(+), 16 deletions(-)
diff --git a/engine/linecontent/linecontent.go b/engine/linecontent/linecontent.go
index db816bd4..c34dc224 100644
--- a/engine/linecontent/linecontent.go
+++ b/engine/linecontent/linecontent.go
@@ -21,12 +21,13 @@ func GetLineContent(line, secret string, startColumn int) (string, error) {
return "", fmt.Errorf("secret empty")
}
- // Truncate lineContent to max size, centering the window on startColumn so a secret
- // far into a very long line isn't truncated away before it can be found.
+ // For lines > lineMaxParseSize, get just the necessary context around the secret
if lineSize > lineMaxParseSize {
+ matchStartIndex := startColumn - 1
windowStart := 0
- if hint := startColumn - 1; hint >= 0 && hint < lineSize {
- windowStart = max(hint-lineMaxParseSize/2, 0)
+ if matchStartIndex >= 0 && matchStartIndex < lineSize {
+ windowStart = max(matchStartIndex-lineMaxParseSize/2, 0)
+ // adjust line context window if windowStart+lineMaxParseSize would be higher than lineSize
windowStart = min(windowStart, lineSize-lineMaxParseSize)
}
line = line[windowStart : windowStart+lineMaxParseSize]
@@ -34,18 +35,14 @@ func GetLineContent(line, secret string, startColumn int) (string, error) {
startColumn -= windowStart
}
- // Find the secret's position in the line, searching from the match's start column
- // onwards first so that repeated occurrences of secret in the line are disambiguated.
+ // The same secret value can appear more than once on a line. Search for it at the relevant index for this secret instance
secretStartIndex := -1
- hint := startColumn - 1
- if hint >= 0 && hint < lineSize {
- if idx := strings.Index(line[hint:], secret); idx != -1 {
- secretStartIndex = hint + idx
+ matchStartIndex := startColumn - 1
+ if matchStartIndex >= 0 && matchStartIndex < lineSize {
+ if idx := strings.Index(line[matchStartIndex:], secret); idx != -1 {
+ secretStartIndex = matchStartIndex + idx
}
}
- if secretStartIndex == -1 {
- secretStartIndex = strings.Index(line, secret)
- }
if secretStartIndex == -1 {
// Secret not found, return truncated content based on context limits
maxSize := contextLeftSizeLimit + contextRightSizeLimit
diff --git a/engine/rules/ruledefine/utils.go b/engine/rules/ruledefine/utils.go
index ec60d7de..9a0d958c 100644
--- a/engine/rules/ruledefine/utils.go
+++ b/engine/rules/ruledefine/utils.go
@@ -27,7 +27,7 @@ const (
// \x60 = `
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
+ 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
)
@@ -46,7 +46,7 @@ func generateSemiGenericRegex(identifiers []string, secretRegex string, isCaseIn
sb.WriteString(operator)
sb.WriteString(secretPrefix)
sb.WriteString(secretRegex)
- sb.WriteString(secretSuffix)
+ sb.WriteString(SecretSuffix)
return regexp.MustCompile(sb.String())
}
@@ -63,7 +63,7 @@ func generateUniqueTokenRegex(secretRegex string, isCaseInsensitive bool) *regex
}
sb.WriteString(secretPrefixUnique)
sb.WriteString(secretRegex)
- sb.WriteString(secretSuffix)
+ sb.WriteString(SecretSuffix)
return regexp.MustCompile(sb.String())
}
diff --git a/pkg/rules.go b/pkg/rules.go
index 87ead50c..e7da1aeb 100644
--- a/pkg/rules.go
+++ b/pkg/rules.go
@@ -8,3 +8,5 @@ import (
func GetDefaultRules(includeDeprecated bool) []*ruledefine.Rule {
return rules.GetDefaultRules(includeDeprecated)
}
+
+func GetRegexSuffix() string { return ruledefine.SecretSuffix }
From 6d7c099fe14b31904a45b12fee67c8cdeba046f0 Mon Sep 17 00:00:00 2001
From: Diogo Rocha <104084969+cx-diogo-rocha@users.noreply.github.com>
Date: Thu, 10 Sep 2026 11:11:25 +0100
Subject: [PATCH 04/12] Remove change from allowLists
---
engine/config.go | 1 -
1 file changed, 1 deletion(-)
diff --git a/engine/config.go b/engine/config.go
index 2b0c630f..4af1444a 100644
--- a/engine/config.go
+++ b/engine/config.go
@@ -37,7 +37,6 @@ var baseConfig = config.Config{
regexp.MustCompile(`verification-metadata\.xml`),
regexp.MustCompile(`Database.refactorlog`),
regexp.MustCompile(`(?:^|/)\.git$`),
- regexp.MustCompile(`(?:^|/)secret\.doc$`),
},
},
},
From 974dabc4a6391c9d439e974a56d18e32b6ea7b05 Mon Sep 17 00:00:00 2001
From: Diogo Rocha <104084969+cx-diogo-rocha@users.noreply.github.com>
Date: Thu, 10 Sep 2026 11:42:08 +0100
Subject: [PATCH 05/12] Fix trivy issues
---
Dockerfile | 4 ++--
go.mod | 8 ++++----
go.sum | 12 ++++++------
3 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 4d582405..399da46e 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.26.4-r0-cb8702a93db4a4@sha256:cb8702a93db4a4b07da9c6e6e93bf412091e7b121b6ade95aa4228ec5fae4301 AS builder
+FROM checkmarx/go:1.27.0-r1-7ff3a27a305109@sha256:7ff3a27a305109341ebf351a1421172d7ee41aeeeb0609451ddb6c8ee5d144b3 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-r4-17886d1320eb5b@sha256:17886d1320eb5b3370f1c4bc4e3f95b5005cd25058ae254d051a85dcde76c33c
+FROM checkmarx/git:2.55.0-r5-d0ccbb0b82fcb8@sha256:d0ccbb0b82fcb8c84ee36087b47eefbf59f8259c4f902fbb3591acd1ee00c546
WORKDIR /app
diff --git a/go.mod b/go.mod
index e0c068cd..90cbc670 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/checkmarx/2ms/v5
-go 1.26.4
+go 1.27.0
replace (
golang.org/x/oauth2 => golang.org/x/oauth2 v0.30.0
@@ -29,9 +29,9 @@ require (
github.com/zricethezav/gitleaks/v8 v8.28.0
go.uber.org/mock v0.5.2
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
- golang.org/x/net v0.56.0
+ golang.org/x/net v0.57.0
golang.org/x/sync v0.22.0
- golang.org/x/text v0.40.0
+ golang.org/x/text v0.41.0
golang.org/x/time v0.5.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -88,6 +88,6 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
- golang.org/x/crypto v0.54.0 // indirect
+ golang.org/x/crypto v0.56.0 // indirect
golang.org/x/sys v0.47.0 // indirect
)
diff --git a/go.sum b/go.sum
index dcb1aaf5..3fc6e89a 100644
--- a/go.sum
+++ b/go.sum
@@ -1252,8 +1252,8 @@ golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn5
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
-golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
-golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
+golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
+golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1387,8 +1387,8 @@ golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
-golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
-golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
+golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
+golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1558,8 +1558,8 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
-golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
-golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
+golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
+golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
From 3e89092bb25d525600e2bfbf3c9d6128dd17bfb5 Mon Sep 17 00:00:00 2001
From: Diogo Rocha <104084969+cx-diogo-rocha@users.noreply.github.com>
Date: Fri, 11 Sep 2026 17:03:12 +0100
Subject: [PATCH 06/12] Review change to GetRegexSuffix
---
pkg/rules.go | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/pkg/rules.go b/pkg/rules.go
index e7da1aeb..567fb94a 100644
--- a/pkg/rules.go
+++ b/pkg/rules.go
@@ -9,4 +9,6 @@ func GetDefaultRules(includeDeprecated bool) []*ruledefine.Rule {
return rules.GetDefaultRules(includeDeprecated)
}
-func GetRegexSuffix() string { return ruledefine.SecretSuffix }
+func GetRegexSuffix() string {
+ return ruledefine.SecretSuffix
+}
From ae8f9dbecd7e731697fc87088d013e08d08713a6 Mon Sep 17 00:00:00 2001
From: Diogo Rocha <104084969+cx-diogo-rocha@users.noreply.github.com>
Date: Fri, 11 Sep 2026 17:11:26 +0100
Subject: [PATCH 07/12] Update unit test that was failing due to update to go
1.27
---
cmd/config_test.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/config_test.go b/cmd/config_test.go
index 3c0a8477..f33a0f71 100644
--- a/cmd/config_test.go
+++ b/cmd/config_test.go
@@ -350,7 +350,7 @@ func TestCustomRulesFlag(t *testing.T) {
name: "Invalid rule type",
customRulesFile: "testData/customRulesInvalidRuleType.json",
expectedRules: nil,
- expectErrors: []error{fmt.Errorf("cannot unmarshal number -2 into Go struct field Rule.scoreRuleType of type uint8")},
+ expectErrors: []error{fmt.Errorf("cannot unmarshal number -2"), fmt.Errorf("scoreRuleType")},
},
}
From a3aa9a38d101712ab7f94a58947c6a06fb446e49 Mon Sep 17 00:00:00 2001
From: Diogo Rocha <104084969+cx-diogo-rocha@users.noreply.github.com>
Date: Mon, 14 Sep 2026 16:21:17 +0100
Subject: [PATCH 08/12] Remove export of GetRegexSuffix
---
pkg/rules.go | 4 ----
1 file changed, 4 deletions(-)
diff --git a/pkg/rules.go b/pkg/rules.go
index 567fb94a..87ead50c 100644
--- a/pkg/rules.go
+++ b/pkg/rules.go
@@ -8,7 +8,3 @@ import (
func GetDefaultRules(includeDeprecated bool) []*ruledefine.Rule {
return rules.GetDefaultRules(includeDeprecated)
}
-
-func GetRegexSuffix() string {
- return ruledefine.SecretSuffix
-}
From 7ffc56ebdb3c3b5128e63a5a050279d6386d851c Mon Sep 17 00:00:00 2001
From: Diogo Rocha <104084969+cx-diogo-rocha@users.noreply.github.com>
Date: Mon, 14 Sep 2026 15:09:56 +0100
Subject: [PATCH 09/12] remove suffix characters from end column calculation
---
engine/engine.go | 44 +++++++++++++++++++++++++-
engine/engine_test.go | 53 +++++++++++++++++++++++++++++++-
engine/rules/ruledefine/utils.go | 4 +--
3 files changed, 97 insertions(+), 4 deletions(-)
diff --git a/engine/engine.go b/engine/engine.go
index 9f696971..572e2a18 100644
--- a/engine/engine.go
+++ b/engine/engine.go
@@ -527,6 +527,43 @@ 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 (stripping the capture
+// group's closing paren and anchoring to the end) rather than duplicating their character
+// classes, so they can't drift out of sync with the source patterns. Both are checked
+// independently -- rather than relying on SecretSuffixIncludingXml always being a superset of
+// SecretSuffix -- so a future divergence between the two can't hide a real overlap.
+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, which are not part of the
+// secret itself. line and endColumn must correspond to each other, i.e. endColumn must be a
+// valid 1-based, inclusive column within line.
+//
+// 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 {
+ if loc := re.FindStringIndex(head); loc != nil {
+ if matched := loc[1] - loc[0]; matched > overlap {
+ overlap = matched
+ }
+ }
+ }
+ return endColumn - overlap
+}
+
// buildSecret creates a secret object from the given source item and finding
func buildSecret(
ctx context.Context,
@@ -547,13 +584,18 @@ func buildSecret(
hasNewline := strings.HasPrefix(value.Line, "\n")
+ // Rule regexes (ruledefine.SecretSuffix / secretSuffixIncludingXml) include a
+ // trailing boundary match after the secret's capture group, so EndColumn as reported
+ // by the detector can extend past the secret's actual last character. Trim that
+ // overlap before it's used for anything else below.
+ 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..aa8c4156 100644
--- a/engine/engine_test.go
+++ b/engine/engine_test.go
@@ -476,9 +476,30 @@ func TestDetectChunks(t *testing.T) {
func TestSecretsColumnIndex(t *testing.T) {
+ const defaultSecret = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
+
+ // True positive from ruledefine's generic-api-key (Generic-Api-Key / GenericCredential)
+ // rule, which is built with generateSemiGenericRegexIncludingXml. Its secret-suffix,
+ // SecretSuffixIncludingXml, matches "" right after the secret with no
+ // whitespace in between (see engine/rules/ruledefine/generic_credential_test.go).
+ xmlSuffixSecret := "AIzaSyATDL7Wz3Ze6BU31Yv3fVVth30Skyib29g"
+ xmlSuffixLine := "" + xmlSuffixSecret + ""
+ xmlSuffixSecretStart := len("") + 1
+ xmlSuffixSecretEnd := xmlSuffixSecretStart + len(xmlSuffixSecret) - 1
+
+ // True positive from ruledefine's Adafruit API Key rule, which is built with the plain
+ // generateSemiGenericRegex/SecretSuffix. The secret sits inside a JSON string value, so
+ // it's followed by a literal (2-character) "\n" escape sequence, not an actual newline
+ // byte (see engine/rules/ruledefine/adafruit_test.go).
+ escapedNewlineSecret := "5qnwhukyv3wi7h9etbfrswi6l8yiwhjl"
+ escapedNewlineLine := `{"config.ini": "ADAFRUIT_TOKEN=` + escapedNewlineSecret + `\nBACKUP_ENABLED=true"}`
+ escapedNewlineSecretStart := strings.Index(escapedNewlineLine, escapedNewlineSecret) + 1
+ escapedNewlineSecretEnd := escapedNewlineSecretStart + len(escapedNewlineSecret) - 1
+
tests := []struct {
name string
lineContent string
+ secret string
startColumn int
endColumn int
expectedLineContent string
@@ -488,6 +509,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 +519,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 +529,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 +539,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 +549,38 @@ 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,
},
+ {
+ // EndColumn as reported by the detector includes the "" that
+ // SecretSuffixIncludingXml matched after the secret; buildSecret must trim it
+ // back to the secret's actual last character.
+ name: "generic-api-key xml suffix consumes closing tag",
+ lineContent: xmlSuffixLine,
+ secret: xmlSuffixSecret,
+ startColumn: xmlSuffixSecretStart,
+ endColumn: xmlSuffixSecretEnd + len(""),
+ expectedLineContent: xmlSuffixLine,
+ expectedStartColumn: xmlSuffixSecretStart,
+ expectedEndColumn: xmlSuffixSecretEnd,
+ },
+ {
+ // EndColumn as reported by the detector includes the literal two-character
+ // "\n" that SecretSuffix's `\\[nr]` alternative matched after the secret.
+ name: "adafruit key followed by literal backslash-n",
+ lineContent: escapedNewlineLine,
+ secret: escapedNewlineSecret,
+ startColumn: escapedNewlineSecretStart,
+ endColumn: escapedNewlineSecretEnd + len(`\n`),
+ expectedLineContent: escapedNewlineLine,
+ expectedStartColumn: escapedNewlineSecretStart,
+ expectedEndColumn: escapedNewlineSecretEnd,
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -539,7 +590,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 9a0d958c..ce210a9f 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())
}
From c56626c0841b655aa0024fcd0d2ceecccef6bae8 Mon Sep 17 00:00:00 2001
From: Diogo Rocha <104084969+cx-diogo-rocha@users.noreply.github.com>
Date: Mon, 14 Sep 2026 17:08:33 +0100
Subject: [PATCH 10/12] Updated tests
---
engine/engine_test.go | 86 +++++++++++++++++++++++++++++++------------
1 file changed, 63 insertions(+), 23 deletions(-)
diff --git a/engine/engine_test.go b/engine/engine_test.go
index aa8c4156..6094e260 100644
--- a/engine/engine_test.go
+++ b/engine/engine_test.go
@@ -478,23 +478,12 @@ func TestSecretsColumnIndex(t *testing.T) {
const defaultSecret = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
- // True positive from ruledefine's generic-api-key (Generic-Api-Key / GenericCredential)
- // rule, which is built with generateSemiGenericRegexIncludingXml. Its secret-suffix,
- // SecretSuffixIncludingXml, matches "" right after the secret with no
- // whitespace in between (see engine/rules/ruledefine/generic_credential_test.go).
- xmlSuffixSecret := "AIzaSyATDL7Wz3Ze6BU31Yv3fVVth30Skyib29g"
+ xmlSuffixSecret := "AIzaSyATD"
xmlSuffixLine := "" + xmlSuffixSecret + ""
xmlSuffixSecretStart := len("") + 1
xmlSuffixSecretEnd := xmlSuffixSecretStart + len(xmlSuffixSecret) - 1
- // True positive from ruledefine's Adafruit API Key rule, which is built with the plain
- // generateSemiGenericRegex/SecretSuffix. The secret sits inside a JSON string value, so
- // it's followed by a literal (2-character) "\n" escape sequence, not an actual newline
- // byte (see engine/rules/ruledefine/adafruit_test.go).
- escapedNewlineSecret := "5qnwhukyv3wi7h9etbfrswi6l8yiwhjl"
- escapedNewlineLine := `{"config.ini": "ADAFRUIT_TOKEN=` + escapedNewlineSecret + `\nBACKUP_ENABLED=true"}`
- escapedNewlineSecretStart := strings.Index(escapedNewlineLine, escapedNewlineSecret) + 1
- escapedNewlineSecretEnd := escapedNewlineSecretStart + len(escapedNewlineSecret) - 1
+ generalSuffixSecret := "5qnwhuk"
tests := []struct {
name string
@@ -570,16 +559,67 @@ func TestSecretsColumnIndex(t *testing.T) {
expectedEndColumn: xmlSuffixSecretEnd,
},
{
- // EndColumn as reported by the detector includes the literal two-character
- // "\n" that SecretSuffix's `\\[nr]` alternative matched after the secret.
- name: "adafruit key followed by literal backslash-n",
- lineContent: escapedNewlineLine,
- secret: escapedNewlineSecret,
- startColumn: escapedNewlineSecretStart,
- endColumn: escapedNewlineSecretEnd + len(`\n`),
- expectedLineContent: escapedNewlineLine,
- expectedStartColumn: escapedNewlineSecretStart,
- expectedEndColumn: escapedNewlineSecretEnd,
+ // EndColumn as reported by the detector includes the trailing real carriage
+ // return that SecretSuffix's `\s` alternative matched after the secret.
+ 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),
+ },
+ {
+ // EndColumn as reported by the detector includes the trailing real newline
+ // that SecretSuffix's `\s` alternative matched after the secret.
+ name: "secret followed by newline",
+ lineContent: generalSuffixSecret + "\n",
+ secret: generalSuffixSecret,
+ startColumn: 1,
+ endColumn: len(generalSuffixSecret + "\n"),
+ expectedLineContent: generalSuffixSecret + "\n",
+ expectedStartColumn: 1,
+ expectedEndColumn: len(generalSuffixSecret),
+ },
+ {
+ // EndColumn as reported by the detector includes the trailing ";" that
+ // SecretSuffix's character-class alternative matched after the secret.
+ name: "secret followed by semicolon",
+ lineContent: generalSuffixSecret + ";",
+ secret: generalSuffixSecret,
+ startColumn: 1,
+ endColumn: len(generalSuffixSecret + ";"),
+ expectedLineContent: generalSuffixSecret + ";",
+ expectedStartColumn: 1,
+ expectedEndColumn: len(generalSuffixSecret),
+ },
+ {
+ // EndColumn as reported by the detector includes the trailing quote that
+ // SecretSuffix's character-class alternative matched after the secret.
+ name: `secret followed by double quote`,
+ lineContent: generalSuffixSecret + `"`,
+ secret: generalSuffixSecret,
+ startColumn: 1,
+ endColumn: len(generalSuffixSecret + `"`),
+ expectedLineContent: generalSuffixSecret + `"`,
+ expectedStartColumn: 1,
+ expectedEndColumn: len(generalSuffixSecret),
+ },
+ {
+ // EndColumn as reported by the detector includes only the last suffix unit
+ // matched: SecretSuffix's suffix group is matched once (not repeated), so of
+ // the two literal escapes here, only the trailing "\n" is ever consumed by the
+ // detector -- and so only it gets trimmed. The earlier literal "\r" escape is
+ // left counted in EndColumn; this is a known limitation, not the ideal result.
+ 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 {
From c526f51cbae1495e88da4c4533fd8d3248ef2a80 Mon Sep 17 00:00:00 2001
From: Diogo Rocha <104084969+cx-diogo-rocha@users.noreply.github.com>
Date: Mon, 14 Sep 2026 17:20:54 +0100
Subject: [PATCH 11/12] Delete spurious comments
---
engine/engine.go | 22 +++++++---------------
engine/engine_test.go | 16 ----------------
2 files changed, 7 insertions(+), 31 deletions(-)
diff --git a/engine/engine.go b/engine/engine.go
index 572e2a18..6c2e330b 100644
--- a/engine/engine.go
+++ b/engine/engine.go
@@ -529,20 +529,14 @@ 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 (stripping the capture
-// group's closing paren and anchoring to the end) rather than duplicating their character
-// classes, so they can't drift out of sync with the source patterns. Both are checked
-// independently -- rather than relying on SecretSuffixIncludingXml always being a superset of
-// SecretSuffix -- so a future divergence between the two can't hide a real overlap.
+// 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, which are not part of the
-// secret itself. line and endColumn must correspond to each other, i.e. endColumn must be a
-// valid 1-based, inclusive column within line.
+// 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
@@ -555,9 +549,11 @@ func trimSecretSuffixOverlap(line string, endColumn int) int {
overlap := 0
for _, re := range secretSuffixTailRegexes {
- if loc := re.FindStringIndex(head); loc != nil {
- if matched := loc[1] - loc[0]; matched > overlap {
- overlap = matched
+ matches := re.FindStringIndex(head)
+ if matches != nil {
+ matchedSuffixLength := matches[1] - matches[0]
+ if matchedSuffixLength > overlap {
+ overlap = matchedSuffixLength
}
}
}
@@ -584,10 +580,6 @@ func buildSecret(
hasNewline := strings.HasPrefix(value.Line, "\n")
- // Rule regexes (ruledefine.SecretSuffix / secretSuffixIncludingXml) include a
- // trailing boundary match after the secret's capture group, so EndColumn as reported
- // by the detector can extend past the secret's actual last character. Trim that
- // overlap before it's used for anything else below.
adjustedEndColumn := trimSecretSuffixOverlap(value.Line, value.EndColumn)
if hasNewline {
diff --git a/engine/engine_test.go b/engine/engine_test.go
index 6094e260..30e3ed49 100644
--- a/engine/engine_test.go
+++ b/engine/engine_test.go
@@ -546,9 +546,6 @@ func TestSecretsColumnIndex(t *testing.T) {
expectedEndColumn: 535,
},
{
- // EndColumn as reported by the detector includes the "" that
- // SecretSuffixIncludingXml matched after the secret; buildSecret must trim it
- // back to the secret's actual last character.
name: "generic-api-key xml suffix consumes closing tag",
lineContent: xmlSuffixLine,
secret: xmlSuffixSecret,
@@ -559,8 +556,6 @@ func TestSecretsColumnIndex(t *testing.T) {
expectedEndColumn: xmlSuffixSecretEnd,
},
{
- // EndColumn as reported by the detector includes the trailing real carriage
- // return that SecretSuffix's `\s` alternative matched after the secret.
name: "secret followed by carriage return",
lineContent: generalSuffixSecret + "\r",
secret: generalSuffixSecret,
@@ -571,8 +566,6 @@ func TestSecretsColumnIndex(t *testing.T) {
expectedEndColumn: len(generalSuffixSecret),
},
{
- // EndColumn as reported by the detector includes the trailing real newline
- // that SecretSuffix's `\s` alternative matched after the secret.
name: "secret followed by newline",
lineContent: generalSuffixSecret + "\n",
secret: generalSuffixSecret,
@@ -583,8 +576,6 @@ func TestSecretsColumnIndex(t *testing.T) {
expectedEndColumn: len(generalSuffixSecret),
},
{
- // EndColumn as reported by the detector includes the trailing ";" that
- // SecretSuffix's character-class alternative matched after the secret.
name: "secret followed by semicolon",
lineContent: generalSuffixSecret + ";",
secret: generalSuffixSecret,
@@ -595,8 +586,6 @@ func TestSecretsColumnIndex(t *testing.T) {
expectedEndColumn: len(generalSuffixSecret),
},
{
- // EndColumn as reported by the detector includes the trailing quote that
- // SecretSuffix's character-class alternative matched after the secret.
name: `secret followed by double quote`,
lineContent: generalSuffixSecret + `"`,
secret: generalSuffixSecret,
@@ -607,11 +596,6 @@ func TestSecretsColumnIndex(t *testing.T) {
expectedEndColumn: len(generalSuffixSecret),
},
{
- // EndColumn as reported by the detector includes only the last suffix unit
- // matched: SecretSuffix's suffix group is matched once (not repeated), so of
- // the two literal escapes here, only the trailing "\n" is ever consumed by the
- // detector -- and so only it gets trimmed. The earlier literal "\r" escape is
- // left counted in EndColumn; this is a known limitation, not the ideal result.
name: `secret followed by literal backslash-r backslash-n`,
lineContent: generalSuffixSecret + `\r\n`,
secret: generalSuffixSecret,
From 29398fbd269e010f5871be16da150fe027b7be77 Mon Sep 17 00:00:00 2001
From: Diogo Rocha <104084969+cx-diogo-rocha@users.noreply.github.com>
Date: Wed, 16 Sep 2026 14:19:21 +0100
Subject: [PATCH 12/12] Update dockerfile tags
---
Dockerfile | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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