Skip to content

Commit 4bf7a43

Browse files
Fix line content and private key rule
1 parent 96846c0 commit 4bf7a43

File tree

6 files changed

+324
-14
lines changed

6 files changed

+324
-14
lines changed

engine/engine.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ func (e *Engine) Detect(item plugins.ISourceItem, secretsChannel chan *secrets.S
9494
} else {
9595
startLine = value.StartLine
9696
endLine = value.EndLine
97+
9798
}
9899
secret := &secrets.Secret{
99100
ID: itemId,
@@ -104,7 +105,7 @@ func (e *Engine) Detect(item plugins.ISourceItem, secretsChannel chan *secrets.S
104105
EndLine: endLine,
105106
EndColumn: value.EndColumn,
106107
Value: value.Secret,
107-
LineContent: linecontent.GetLineContent(value.Line, value.StartColumn, value.EndColumn),
108+
LineContent: linecontent.GetLineContent(value.Line, value.Secret),
108109
RuleDescription: value.Description,
109110
}
110111
if !isSecretIgnored(secret, &e.ignoredIds, &e.allowedValues) {

engine/linecontent/linecontent.go

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,93 @@
11
package linecontent
22

33
const (
4-
contextLeftSizeLimit = 250
5-
contextRightSizeLimit = 250
4+
lineContentMaxParseSize = 10000
5+
contextLeftSizeLimit = 250
6+
contextRightSizeLimit = 250
67
)
78

8-
func GetLineContent(lineContent string, startColumn, endColumn int) string {
9-
lineContentSize := len(lineContent)
9+
func GetLineContent(line, secret string) string {
10+
lineSize := len(line)
11+
if lineSize == 0 || len(secret) == 0 {
12+
return ""
13+
}
14+
15+
// Truncate line to max parse size before converting to runes
16+
shouldRemoveLastChars := false
17+
if lineSize > lineContentMaxParseSize {
18+
line = line[:lineContentMaxParseSize]
19+
shouldRemoveLastChars = true // to prevent issues when truncating a multibyte character in the middle
20+
}
21+
22+
// Convert line and secret to runes
23+
lineRunes, lineRunesSize := getLineRunes(line, shouldRemoveLastChars)
24+
secretRunes := []rune(secret)
25+
secretRunesSize := len(secretRunes)
26+
27+
// Find the secret's position in the line (working with runes)
28+
secretStartIndex := indexOf(lineRunes, secretRunes, lineRunesSize, secretRunesSize)
29+
if secretStartIndex == -1 {
30+
// Secret not found, return truncated content based on context limits
31+
maxSize := contextLeftSizeLimit + contextRightSizeLimit
32+
if lineRunesSize < maxSize {
33+
return string(lineRunes)
34+
}
35+
return string(lineRunes[:maxSize])
36+
}
37+
38+
// Calculate bounds for the result
39+
secretEndIndex := secretStartIndex + secretRunesSize
40+
start := maxIndex(secretStartIndex-contextLeftSizeLimit, 0)
41+
end := minIndex(secretEndIndex+contextRightSizeLimit, lineRunesSize)
42+
43+
return string(lineRunes[start:end])
44+
}
1045

11-
startIndex := startColumn - contextLeftSizeLimit
12-
if startIndex < 0 {
13-
startIndex = 0
46+
func getLineRunes(line string, shouldRemoveLastChars bool) ([]rune, int) {
47+
lineRunes := []rune(line)
48+
lineRunesSize := len(lineRunes)
49+
if shouldRemoveLastChars {
50+
// A single rune can be up to 4 bytes in UTF-8 encoding.
51+
// If truncation occurs in the middle of a multibyte character,
52+
// it will leave a partial byte sequence, potentially consisting of
53+
// up to 3 bytes. Each of these remaining bytes will be treated
54+
// as an invalid character, displayed as a replacement character (�).
55+
// To prevent this, we adjust the rune count by removing the last
56+
// 3 runes, ensuring no partial characters are included.
57+
lineRunesSize -= 3
1458
}
59+
return lineRunes[:lineRunesSize], lineRunesSize
60+
}
1561

16-
endIndex := endColumn + contextRightSizeLimit
17-
if endIndex > lineContentSize {
18-
endIndex = lineContentSize
62+
func indexOf(line, secret []rune, lineSize, secretSize int) int {
63+
for i := 0; i <= lineSize-secretSize; i++ {
64+
if compareRunes(line[i:i+secretSize], secret) {
65+
return i
66+
}
1967
}
68+
return -1
69+
}
70+
71+
func compareRunes(a, b []rune) bool {
72+
// a and b must have the same size.
73+
for i := range a {
74+
if a[i] != b[i] {
75+
return false
76+
}
77+
}
78+
return true
79+
}
2080

21-
return lineContent[startIndex:endIndex]
81+
func minIndex(a, b int) int {
82+
if a < b {
83+
return a
84+
}
85+
return b
86+
}
87+
88+
func maxIndex(a, b int) int {
89+
if a > b {
90+
return a
91+
}
92+
return b
2293
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
package linecontent
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
const (
9+
dummySecret = "DummySecret"
10+
)
11+
12+
func TestGetLineContent(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
line string
16+
secret string
17+
expected string
18+
}{
19+
{
20+
name: "Empty line",
21+
line: "",
22+
secret: dummySecret,
23+
expected: "",
24+
},
25+
{
26+
name: "Empty secret",
27+
line: "line",
28+
secret: "",
29+
expected: "",
30+
},
31+
{
32+
name: "Secret not found with line size smaller than the parse limit",
33+
line: "Dummy content line",
34+
secret: dummySecret,
35+
expected: "Dummy content line",
36+
},
37+
{
38+
name: "Secret not found with secret present and line size larger than the parse limit",
39+
line: "This is the start of a big line content" + strings.Repeat("A", lineContentMaxParseSize) + dummySecret,
40+
secret: dummySecret,
41+
expected: "This is the start of a big line content" + strings.Repeat("A", contextLeftSizeLimit+contextRightSizeLimit-len("This is the start of a big line content")),
42+
},
43+
{
44+
name: "Secret larger than the line",
45+
line: strings.Repeat("B", contextLeftSizeLimit) + strings.Repeat("A", contextRightSizeLimit),
46+
secret: "large secret" + strings.Repeat("B", contextRightSizeLimit+contextLeftSizeLimit+100),
47+
expected: strings.Repeat("B", contextLeftSizeLimit) + strings.Repeat("A", contextRightSizeLimit),
48+
},
49+
{
50+
name: "Secret at the beginning with line size smaller than the parse limit",
51+
line: "start:" + dummySecret + strings.Repeat("A", lineContentMaxParseSize/2),
52+
secret: dummySecret,
53+
expected: "start:" + dummySecret + strings.Repeat("A", contextRightSizeLimit),
54+
},
55+
{
56+
name: "Secret found in middle with line size smaller than the parse limit",
57+
line: "start" + strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", contextRightSizeLimit) + "end",
58+
secret: dummySecret,
59+
expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", contextRightSizeLimit),
60+
},
61+
{
62+
name: "Secret at the end with line size smaller than the parse limit",
63+
line: strings.Repeat("A", lineContentMaxParseSize/2) + dummySecret + ":end",
64+
secret: dummySecret,
65+
expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + ":end",
66+
},
67+
{
68+
name: "Secret at the beginning with line size larger than the parse limit",
69+
line: "start:" + dummySecret + strings.Repeat("A", lineContentMaxParseSize),
70+
secret: dummySecret,
71+
expected: "start:" + dummySecret + strings.Repeat("A", contextRightSizeLimit),
72+
},
73+
{
74+
name: "Secret found in middle with line size larger than the parse limit",
75+
line: "start" + strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", lineContentMaxParseSize) + "end",
76+
secret: dummySecret,
77+
expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", contextRightSizeLimit),
78+
},
79+
{
80+
name: "Secret at the end with line size larger than the parse limit",
81+
line: strings.Repeat("A", lineContentMaxParseSize-100) + dummySecret + strings.Repeat("A", lineContentMaxParseSize),
82+
secret: dummySecret,
83+
expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", calculateRepeatForSecretAtTheEndWithLargerThanParseLimit(100, 1, len(dummySecret))),
84+
},
85+
{
86+
name: "Secret at the beginning with line containing 2 byte chars and size smaller than the parse limit",
87+
line: "start:" + dummySecret + strings.Repeat("é", lineContentMaxParseSize/4),
88+
secret: dummySecret,
89+
expected: "start:" + dummySecret + strings.Repeat("é", contextRightSizeLimit),
90+
},
91+
{
92+
name: "Secret found in middle with line containing 2 byte chars and size smaller than the parse limit",
93+
line: "start" + strings.Repeat("é", contextLeftSizeLimit) + dummySecret + strings.Repeat("é", contextRightSizeLimit) + "end",
94+
secret: dummySecret,
95+
expected: strings.Repeat("é", contextLeftSizeLimit) + dummySecret + strings.Repeat("é", contextRightSizeLimit),
96+
},
97+
{
98+
name: "Secret at the end with line containing 2 byte chars and size smaller than the parse limit",
99+
line: strings.Repeat("é", lineContentMaxParseSize/4) + dummySecret + ":end",
100+
secret: dummySecret,
101+
expected: strings.Repeat("é", contextLeftSizeLimit) + dummySecret + ":end",
102+
},
103+
{
104+
name: "Secret at the beginning with line containing 2 byte chars and size larger than the parse limit",
105+
line: "start:" + dummySecret + strings.Repeat("é", lineContentMaxParseSize/2),
106+
secret: dummySecret,
107+
expected: "start:" + dummySecret + strings.Repeat("é", contextRightSizeLimit),
108+
},
109+
{
110+
name: "Secret found in middle with line containing 2 byte chars and size larger than the parse limit",
111+
line: "start" + strings.Repeat("é", contextLeftSizeLimit) + dummySecret + strings.Repeat("é", lineContentMaxParseSize/2) + "end",
112+
secret: dummySecret,
113+
expected: strings.Repeat("é", contextLeftSizeLimit) + dummySecret + strings.Repeat("é", contextRightSizeLimit),
114+
},
115+
{
116+
name: "Secret at the end with line containing 2 byte chars and size larger than the parse limit",
117+
line: strings.Repeat("é", lineContentMaxParseSize/2-100) + dummySecret + strings.Repeat("é", lineContentMaxParseSize/2),
118+
secret: dummySecret,
119+
expected: strings.Repeat("é", contextLeftSizeLimit) + dummySecret + strings.Repeat("é", calculateRepeatForSecretAtTheEndWithLargerThanParseLimit(100, 2, len(dummySecret))),
120+
},
121+
{
122+
name: "Secret at the beginning with line containing 3 byte chars and size smaller than the parse limit",
123+
line: "start:" + dummySecret + strings.Repeat("ࠚ", lineContentMaxParseSize/6),
124+
secret: dummySecret,
125+
expected: "start:" + dummySecret + strings.Repeat("ࠚ", contextRightSizeLimit),
126+
},
127+
{
128+
name: "Secret found in middle with line containing 3 byte chars and size smaller than the parse limit",
129+
line: "start" + strings.Repeat("ࠚ", contextLeftSizeLimit) + dummySecret + strings.Repeat("ࠚ", contextRightSizeLimit) + "end",
130+
secret: dummySecret,
131+
expected: strings.Repeat("ࠚ", contextLeftSizeLimit) + dummySecret + strings.Repeat("ࠚ", contextRightSizeLimit),
132+
},
133+
{
134+
name: "Secret at the end with line containing 3 byte chars and size smaller than the parse limit",
135+
line: strings.Repeat("ࠚ", lineContentMaxParseSize/6) + dummySecret + ":end",
136+
secret: dummySecret,
137+
expected: strings.Repeat("ࠚ", contextLeftSizeLimit) + dummySecret + ":end",
138+
},
139+
{
140+
name: "Secret at the beginning with line containing 3 byte chars and size larger than the parse limit",
141+
line: "start:" + dummySecret + strings.Repeat("ࠚ", lineContentMaxParseSize/3),
142+
secret: dummySecret,
143+
expected: "start:" + dummySecret + strings.Repeat("ࠚ", contextRightSizeLimit),
144+
},
145+
{
146+
name: "Secret found in middle with line containing 3 byte chars and size larger than the parse limit",
147+
line: "start" + strings.Repeat("ࠚ", contextLeftSizeLimit) + dummySecret + strings.Repeat("ࠚ", lineContentMaxParseSize/3) + "end",
148+
secret: dummySecret,
149+
expected: strings.Repeat("ࠚ", contextLeftSizeLimit) + dummySecret + strings.Repeat("ࠚ", contextRightSizeLimit),
150+
},
151+
{
152+
name: "Secret at the end with line containing 3 byte chars and size larger than the parse limit",
153+
line: strings.Repeat("ࠚ", lineContentMaxParseSize/3-100) + dummySecret + strings.Repeat("ࠚ", lineContentMaxParseSize/3),
154+
secret: dummySecret,
155+
expected: strings.Repeat("ࠚ", contextLeftSizeLimit) + dummySecret + strings.Repeat("ࠚ", calculateRepeatForSecretAtTheEndWithLargerThanParseLimit(100, 3, len(dummySecret))),
156+
},
157+
{
158+
name: "Secret at the beginning with line containing 4 byte chars and size smaller than the parse limit",
159+
line: "start:" + dummySecret + strings.Repeat("𝄞", lineContentMaxParseSize/8),
160+
secret: dummySecret,
161+
expected: "start:" + dummySecret + strings.Repeat("𝄞", contextRightSizeLimit),
162+
},
163+
{
164+
name: "Secret found in middle with line containing 4 byte chars and size smaller than the parse limit",
165+
line: "start" + strings.Repeat("𝄞", contextLeftSizeLimit) + dummySecret + strings.Repeat("𝄞", contextRightSizeLimit) + "end",
166+
secret: dummySecret,
167+
expected: strings.Repeat("𝄞", contextLeftSizeLimit) + dummySecret + strings.Repeat("𝄞", contextRightSizeLimit),
168+
},
169+
{
170+
name: "Secret at the end with line containing 4 byte chars and size smaller than the parse limit",
171+
line: strings.Repeat("𝄞", lineContentMaxParseSize/8) + dummySecret + ":end",
172+
secret: dummySecret,
173+
expected: strings.Repeat("𝄞", contextLeftSizeLimit) + dummySecret + ":end",
174+
},
175+
{
176+
name: "Secret at the beginning with line containing 4 byte chars and size larger than the parse limit",
177+
line: "start:" + dummySecret + strings.Repeat("𝄞", lineContentMaxParseSize/4),
178+
secret: dummySecret,
179+
expected: "start:" + dummySecret + strings.Repeat("𝄞", contextRightSizeLimit),
180+
},
181+
{
182+
name: "Secret found in middle with line containing 4 byte chars and size larger than the parse limit",
183+
line: "start" + strings.Repeat("𝄞", contextLeftSizeLimit) + dummySecret + strings.Repeat("𝄞", lineContentMaxParseSize/4) + "end",
184+
secret: dummySecret,
185+
expected: strings.Repeat("𝄞", contextLeftSizeLimit) + dummySecret + strings.Repeat("𝄞", contextRightSizeLimit),
186+
},
187+
{
188+
name: "Secret at the end with line containing 4 byte chars and size larger than the parse limit",
189+
line: strings.Repeat("𝄞", lineContentMaxParseSize/4-100) + dummySecret + strings.Repeat("𝄞", lineContentMaxParseSize/4),
190+
secret: dummySecret,
191+
expected: strings.Repeat("𝄞", contextLeftSizeLimit) + dummySecret + strings.Repeat("𝄞", calculateRepeatForSecretAtTheEndWithLargerThanParseLimit(100, 4, len(dummySecret))),
192+
},
193+
}
194+
195+
for _, tt := range tests {
196+
t.Run(tt.name, func(t *testing.T) {
197+
got := GetLineContent(tt.line, tt.secret)
198+
if got != tt.expected {
199+
t.Errorf("GetLineContent() = %v, want %v", got, tt.expected)
200+
}
201+
})
202+
}
203+
}
204+
205+
func calculateRepeatForSecretAtTheEndWithLargerThanParseLimit(offset, bytes, secretLength int) int {
206+
remainingSize := lineContentMaxParseSize - ((lineContentMaxParseSize/bytes - offset) * bytes) - secretLength
207+
return (remainingSize - ((3 - ((remainingSize) % bytes)) * bytes)) / bytes
208+
}

engine/rules/privateKey.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package rules
2+
3+
import (
4+
"github.com/zricethezav/gitleaks/v8/config"
5+
"regexp"
6+
)
7+
8+
func PrivateKey() *config.Rule {
9+
// define rule
10+
r := config.Rule{
11+
Description: "Identified a Private Key, which may compromise cryptographic security and sensitive data encryption.",
12+
RuleID: "private-key",
13+
Regex: regexp.MustCompile(`(?i)-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----[\s\S-]*?KEY(?: BLOCK)?-----`),
14+
Keywords: []string{"-----BEGIN"},
15+
}
16+
17+
// validate
18+
tps := []string{`-----BEGIN PRIVATE KEY-----
19+
anything
20+
-----END PRIVATE KEY-----`,
21+
`-----BEGIN RSA PRIVATE KEY-----
22+
abcdefghijklmnopqrstuvwxyz
23+
-----END RSA PRIVATE KEY-----
24+
`,
25+
`-----BEGIN PRIVATE KEY BLOCK-----
26+
anything
27+
-----END PRIVATE KEY BLOCK-----`,
28+
} // gitleaks:allow
29+
return validate(r, tps, nil)
30+
}

engine/rules/rules.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ func getDefaultRules() *[]Rule {
187187
{Rule: *rules.PlanetScaleOAuthToken(), Tags: []string{TagAccessToken}, ScoreParameters: ScoreParameters{Category: CategoryDatabaseAsAService, RuleType: 4}},
188188
{Rule: *rules.PostManAPI(), Tags: []string{TagApiToken}, ScoreParameters: ScoreParameters{Category: CategoryAPIAccess, RuleType: 4}},
189189
{Rule: *rules.Prefect(), Tags: []string{TagApiToken}, ScoreParameters: ScoreParameters{Category: CategoryAPIAccess, RuleType: 4}},
190-
{Rule: *rules.PrivateKey(), Tags: []string{TagPrivateKey}, ScoreParameters: ScoreParameters{Category: CategoryGeneralOrUnknown, RuleType: 4}},
190+
{Rule: *PrivateKey(), Tags: []string{TagPrivateKey}, ScoreParameters: ScoreParameters{Category: CategoryGeneralOrUnknown, RuleType: 4}},
191191
{Rule: *rules.PulumiAPIToken(), Tags: []string{TagApiToken}, ScoreParameters: ScoreParameters{Category: CategoryCloudPlatform, RuleType: 4}},
192192
{Rule: *rules.PyPiUploadToken(), Tags: []string{TagUploadToken}, ScoreParameters: ScoreParameters{Category: CategoryPackageManagement, RuleType: 4}},
193193
{Rule: *rules.RapidAPIAccessToken(), Tags: []string{TagAccessToken}, ScoreParameters: ScoreParameters{Category: CategoryAPIAccess, RuleType: 4}},

engine/score/score_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func TestScore(t *testing.T) {
135135
ruleConfig.PlanetScaleOAuthToken().RuleID: {10, 5.2, 8.2},
136136
ruleConfig.PostManAPI().RuleID: {10, 5.2, 8.2},
137137
ruleConfig.Prefect().RuleID: {10, 5.2, 8.2},
138-
ruleConfig.PrivateKey().RuleID: {10, 5.2, 8.2},
138+
rules.PrivateKey().RuleID: {10, 5.2, 8.2},
139139
ruleConfig.PulumiAPIToken().RuleID: {10, 5.2, 8.2},
140140
ruleConfig.PyPiUploadToken().RuleID: {10, 5.2, 8.2},
141141
ruleConfig.RapidAPIAccessToken().RuleID: {10, 5.2, 8.2},

0 commit comments

Comments
 (0)