Skip to content

Commit

Permalink
Fix line content and private key rule
Browse files Browse the repository at this point in the history
  • Loading branch information
LeonardoLordelloFontes committed Jan 17, 2025
1 parent 96846c0 commit 4bf7a43
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 14 deletions.
3 changes: 2 additions & 1 deletion engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func (e *Engine) Detect(item plugins.ISourceItem, secretsChannel chan *secrets.S
} else {
startLine = value.StartLine
endLine = value.EndLine

}
secret := &secrets.Secret{
ID: itemId,
Expand All @@ -104,7 +105,7 @@ func (e *Engine) Detect(item plugins.ISourceItem, secretsChannel chan *secrets.S
EndLine: endLine,
EndColumn: value.EndColumn,
Value: value.Secret,
LineContent: linecontent.GetLineContent(value.Line, value.StartColumn, value.EndColumn),
LineContent: linecontent.GetLineContent(value.Line, value.Secret),
RuleDescription: value.Description,
}
if !isSecretIgnored(secret, &e.ignoredIds, &e.allowedValues) {
Expand Down
93 changes: 82 additions & 11 deletions engine/linecontent/linecontent.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,93 @@
package linecontent

const (
contextLeftSizeLimit = 250
contextRightSizeLimit = 250
lineContentMaxParseSize = 10000
contextLeftSizeLimit = 250
contextRightSizeLimit = 250
)

func GetLineContent(lineContent string, startColumn, endColumn int) string {
lineContentSize := len(lineContent)
func GetLineContent(line, secret string) string {
lineSize := len(line)
if lineSize == 0 || len(secret) == 0 {
return ""
}

// Truncate line to max parse size before converting to runes
shouldRemoveLastChars := false
if lineSize > lineContentMaxParseSize {
line = line[:lineContentMaxParseSize]
shouldRemoveLastChars = true // to prevent issues when truncating a multibyte character in the middle
}

// Convert line and secret to runes
lineRunes, lineRunesSize := getLineRunes(line, shouldRemoveLastChars)
secretRunes := []rune(secret)
secretRunesSize := len(secretRunes)

// Find the secret's position in the line (working with runes)
secretStartIndex := indexOf(lineRunes, secretRunes, lineRunesSize, secretRunesSize)
if secretStartIndex == -1 {
// Secret not found, return truncated content based on context limits
maxSize := contextLeftSizeLimit + contextRightSizeLimit
if lineRunesSize < maxSize {
return string(lineRunes)
}
return string(lineRunes[:maxSize])
}

// Calculate bounds for the result
secretEndIndex := secretStartIndex + secretRunesSize
start := maxIndex(secretStartIndex-contextLeftSizeLimit, 0)
end := minIndex(secretEndIndex+contextRightSizeLimit, lineRunesSize)

return string(lineRunes[start:end])
}

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

endIndex := endColumn + contextRightSizeLimit
if endIndex > lineContentSize {
endIndex = lineContentSize
func indexOf(line, secret []rune, lineSize, secretSize int) int {
for i := 0; i <= lineSize-secretSize; i++ {
if compareRunes(line[i:i+secretSize], secret) {
return i
}
}
return -1
}

func compareRunes(a, b []rune) bool {
// a and b must have the same size.
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

return lineContent[startIndex:endIndex]
func minIndex(a, b int) int {
if a < b {
return a
}
return b
}

func maxIndex(a, b int) int {
if a > b {
return a
}
return b
}
208 changes: 208 additions & 0 deletions engine/linecontent/linecontent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package linecontent

import (
"strings"
"testing"
)

const (
dummySecret = "DummySecret"
)

func TestGetLineContent(t *testing.T) {
tests := []struct {
name string
line string
secret string
expected string
}{
{
name: "Empty line",
line: "",
secret: dummySecret,
expected: "",
},
{
name: "Empty secret",
line: "line",
secret: "",
expected: "",
},
{
name: "Secret not found with line size smaller than the parse limit",
line: "Dummy content line",
secret: dummySecret,
expected: "Dummy content line",
},
{
name: "Secret not found with secret present and line size larger than the parse limit",
line: "This is the start of a big line content" + strings.Repeat("A", lineContentMaxParseSize) + dummySecret,
secret: dummySecret,
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")),
},
{
name: "Secret larger than the line",
line: strings.Repeat("B", contextLeftSizeLimit) + strings.Repeat("A", contextRightSizeLimit),
secret: "large secret" + strings.Repeat("B", contextRightSizeLimit+contextLeftSizeLimit+100),
expected: strings.Repeat("B", contextLeftSizeLimit) + strings.Repeat("A", contextRightSizeLimit),
},
{
name: "Secret at the beginning with line size smaller than the parse limit",
line: "start:" + dummySecret + strings.Repeat("A", lineContentMaxParseSize/2),
secret: dummySecret,
expected: "start:" + dummySecret + strings.Repeat("A", contextRightSizeLimit),
},
{
name: "Secret found in middle with line size smaller than the parse limit",
line: "start" + strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", contextRightSizeLimit) + "end",
secret: dummySecret,
expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", contextRightSizeLimit),
},
{
name: "Secret at the end with line size smaller than the parse limit",
line: strings.Repeat("A", lineContentMaxParseSize/2) + dummySecret + ":end",
secret: dummySecret,
expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + ":end",
},
{
name: "Secret at the beginning with line size larger than the parse limit",
line: "start:" + dummySecret + strings.Repeat("A", lineContentMaxParseSize),
secret: dummySecret,
expected: "start:" + dummySecret + strings.Repeat("A", contextRightSizeLimit),
},
{
name: "Secret found in middle with line size larger than the parse limit",
line: "start" + strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", lineContentMaxParseSize) + "end",
secret: dummySecret,
expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", contextRightSizeLimit),
},
{
name: "Secret at the end with line size larger than the parse limit",
line: strings.Repeat("A", lineContentMaxParseSize-100) + dummySecret + strings.Repeat("A", lineContentMaxParseSize),
secret: dummySecret,
expected: strings.Repeat("A", contextLeftSizeLimit) + dummySecret + strings.Repeat("A", calculateRepeatForSecretAtTheEndWithLargerThanParseLimit(100, 1, len(dummySecret))),
},
{
name: "Secret at the beginning with line containing 2 byte chars and size smaller than the parse limit",
line: "start:" + dummySecret + strings.Repeat("é", lineContentMaxParseSize/4),
secret: dummySecret,
expected: "start:" + dummySecret + strings.Repeat("é", contextRightSizeLimit),
},
{
name: "Secret found in middle with line containing 2 byte chars and size smaller than the parse limit",
line: "start" + strings.Repeat("é", contextLeftSizeLimit) + dummySecret + strings.Repeat("é", contextRightSizeLimit) + "end",
secret: dummySecret,
expected: strings.Repeat("é", contextLeftSizeLimit) + dummySecret + strings.Repeat("é", contextRightSizeLimit),
},
{
name: "Secret at the end with line containing 2 byte chars and size smaller than the parse limit",
line: strings.Repeat("é", lineContentMaxParseSize/4) + dummySecret + ":end",
secret: dummySecret,
expected: strings.Repeat("é", contextLeftSizeLimit) + dummySecret + ":end",
},
{
name: "Secret at the beginning with line containing 2 byte chars and size larger than the parse limit",
line: "start:" + dummySecret + strings.Repeat("é", lineContentMaxParseSize/2),
secret: dummySecret,
expected: "start:" + dummySecret + strings.Repeat("é", contextRightSizeLimit),
},
{
name: "Secret found in middle with line containing 2 byte chars and size larger than the parse limit",
line: "start" + strings.Repeat("é", contextLeftSizeLimit) + dummySecret + strings.Repeat("é", lineContentMaxParseSize/2) + "end",
secret: dummySecret,
expected: strings.Repeat("é", contextLeftSizeLimit) + dummySecret + strings.Repeat("é", contextRightSizeLimit),
},
{
name: "Secret at the end with line containing 2 byte chars and size larger than the parse limit",
line: strings.Repeat("é", lineContentMaxParseSize/2-100) + dummySecret + strings.Repeat("é", lineContentMaxParseSize/2),
secret: dummySecret,
expected: strings.Repeat("é", contextLeftSizeLimit) + dummySecret + strings.Repeat("é", calculateRepeatForSecretAtTheEndWithLargerThanParseLimit(100, 2, len(dummySecret))),
},
{
name: "Secret at the beginning with line containing 3 byte chars and size smaller than the parse limit",
line: "start:" + dummySecret + strings.Repeat("ࠚ", lineContentMaxParseSize/6),
secret: dummySecret,
expected: "start:" + dummySecret + strings.Repeat("ࠚ", contextRightSizeLimit),
},
{
name: "Secret found in middle with line containing 3 byte chars and size smaller than the parse limit",
line: "start" + strings.Repeat("ࠚ", contextLeftSizeLimit) + dummySecret + strings.Repeat("ࠚ", contextRightSizeLimit) + "end",
secret: dummySecret,
expected: strings.Repeat("ࠚ", contextLeftSizeLimit) + dummySecret + strings.Repeat("ࠚ", contextRightSizeLimit),
},
{
name: "Secret at the end with line containing 3 byte chars and size smaller than the parse limit",
line: strings.Repeat("ࠚ", lineContentMaxParseSize/6) + dummySecret + ":end",
secret: dummySecret,
expected: strings.Repeat("ࠚ", contextLeftSizeLimit) + dummySecret + ":end",
},
{
name: "Secret at the beginning with line containing 3 byte chars and size larger than the parse limit",
line: "start:" + dummySecret + strings.Repeat("ࠚ", lineContentMaxParseSize/3),
secret: dummySecret,
expected: "start:" + dummySecret + strings.Repeat("ࠚ", contextRightSizeLimit),
},
{
name: "Secret found in middle with line containing 3 byte chars and size larger than the parse limit",
line: "start" + strings.Repeat("ࠚ", contextLeftSizeLimit) + dummySecret + strings.Repeat("ࠚ", lineContentMaxParseSize/3) + "end",
secret: dummySecret,
expected: strings.Repeat("ࠚ", contextLeftSizeLimit) + dummySecret + strings.Repeat("ࠚ", contextRightSizeLimit),
},
{
name: "Secret at the end with line containing 3 byte chars and size larger than the parse limit",
line: strings.Repeat("ࠚ", lineContentMaxParseSize/3-100) + dummySecret + strings.Repeat("ࠚ", lineContentMaxParseSize/3),
secret: dummySecret,
expected: strings.Repeat("ࠚ", contextLeftSizeLimit) + dummySecret + strings.Repeat("ࠚ", calculateRepeatForSecretAtTheEndWithLargerThanParseLimit(100, 3, len(dummySecret))),
},
{
name: "Secret at the beginning with line containing 4 byte chars and size smaller than the parse limit",
line: "start:" + dummySecret + strings.Repeat("𝄞", lineContentMaxParseSize/8),
secret: dummySecret,
expected: "start:" + dummySecret + strings.Repeat("𝄞", contextRightSizeLimit),
},
{
name: "Secret found in middle with line containing 4 byte chars and size smaller than the parse limit",
line: "start" + strings.Repeat("𝄞", contextLeftSizeLimit) + dummySecret + strings.Repeat("𝄞", contextRightSizeLimit) + "end",
secret: dummySecret,
expected: strings.Repeat("𝄞", contextLeftSizeLimit) + dummySecret + strings.Repeat("𝄞", contextRightSizeLimit),
},
{
name: "Secret at the end with line containing 4 byte chars and size smaller than the parse limit",
line: strings.Repeat("𝄞", lineContentMaxParseSize/8) + dummySecret + ":end",
secret: dummySecret,
expected: strings.Repeat("𝄞", contextLeftSizeLimit) + dummySecret + ":end",
},
{
name: "Secret at the beginning with line containing 4 byte chars and size larger than the parse limit",
line: "start:" + dummySecret + strings.Repeat("𝄞", lineContentMaxParseSize/4),
secret: dummySecret,
expected: "start:" + dummySecret + strings.Repeat("𝄞", contextRightSizeLimit),
},
{
name: "Secret found in middle with line containing 4 byte chars and size larger than the parse limit",
line: "start" + strings.Repeat("𝄞", contextLeftSizeLimit) + dummySecret + strings.Repeat("𝄞", lineContentMaxParseSize/4) + "end",
secret: dummySecret,
expected: strings.Repeat("𝄞", contextLeftSizeLimit) + dummySecret + strings.Repeat("𝄞", contextRightSizeLimit),
},
{
name: "Secret at the end with line containing 4 byte chars and size larger than the parse limit",
line: strings.Repeat("𝄞", lineContentMaxParseSize/4-100) + dummySecret + strings.Repeat("𝄞", lineContentMaxParseSize/4),
secret: dummySecret,
expected: strings.Repeat("𝄞", contextLeftSizeLimit) + dummySecret + strings.Repeat("𝄞", calculateRepeatForSecretAtTheEndWithLargerThanParseLimit(100, 4, len(dummySecret))),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetLineContent(tt.line, tt.secret)
if got != tt.expected {
t.Errorf("GetLineContent() = %v, want %v", got, tt.expected)
}
})
}
}

func calculateRepeatForSecretAtTheEndWithLargerThanParseLimit(offset, bytes, secretLength int) int {
remainingSize := lineContentMaxParseSize - ((lineContentMaxParseSize/bytes - offset) * bytes) - secretLength
return (remainingSize - ((3 - ((remainingSize) % bytes)) * bytes)) / bytes
}
30 changes: 30 additions & 0 deletions engine/rules/privateKey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package rules

import (
"github.com/zricethezav/gitleaks/v8/config"
"regexp"
)

func PrivateKey() *config.Rule {
// define rule
r := config.Rule{
Description: "Identified a Private Key, which may compromise cryptographic security and sensitive data encryption.",
RuleID: "private-key",
Regex: regexp.MustCompile(`(?i)-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----[\s\S-]*?KEY(?: BLOCK)?-----`),
Keywords: []string{"-----BEGIN"},
}

// validate
tps := []string{`-----BEGIN PRIVATE KEY-----
anything
-----END PRIVATE KEY-----`,
`-----BEGIN RSA PRIVATE KEY-----
abcdefghijklmnopqrstuvwxyz
-----END RSA PRIVATE KEY-----
`,
`-----BEGIN PRIVATE KEY BLOCK-----
anything
-----END PRIVATE KEY BLOCK-----`,
} // gitleaks:allow
return validate(r, tps, nil)
}
2 changes: 1 addition & 1 deletion engine/rules/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func getDefaultRules() *[]Rule {
{Rule: *rules.PlanetScaleOAuthToken(), Tags: []string{TagAccessToken}, ScoreParameters: ScoreParameters{Category: CategoryDatabaseAsAService, RuleType: 4}},
{Rule: *rules.PostManAPI(), Tags: []string{TagApiToken}, ScoreParameters: ScoreParameters{Category: CategoryAPIAccess, RuleType: 4}},
{Rule: *rules.Prefect(), Tags: []string{TagApiToken}, ScoreParameters: ScoreParameters{Category: CategoryAPIAccess, RuleType: 4}},
{Rule: *rules.PrivateKey(), Tags: []string{TagPrivateKey}, ScoreParameters: ScoreParameters{Category: CategoryGeneralOrUnknown, RuleType: 4}},
{Rule: *PrivateKey(), Tags: []string{TagPrivateKey}, ScoreParameters: ScoreParameters{Category: CategoryGeneralOrUnknown, RuleType: 4}},
{Rule: *rules.PulumiAPIToken(), Tags: []string{TagApiToken}, ScoreParameters: ScoreParameters{Category: CategoryCloudPlatform, RuleType: 4}},
{Rule: *rules.PyPiUploadToken(), Tags: []string{TagUploadToken}, ScoreParameters: ScoreParameters{Category: CategoryPackageManagement, RuleType: 4}},
{Rule: *rules.RapidAPIAccessToken(), Tags: []string{TagAccessToken}, ScoreParameters: ScoreParameters{Category: CategoryAPIAccess, RuleType: 4}},
Expand Down
2 changes: 1 addition & 1 deletion engine/score/score_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func TestScore(t *testing.T) {
ruleConfig.PlanetScaleOAuthToken().RuleID: {10, 5.2, 8.2},
ruleConfig.PostManAPI().RuleID: {10, 5.2, 8.2},
ruleConfig.Prefect().RuleID: {10, 5.2, 8.2},
ruleConfig.PrivateKey().RuleID: {10, 5.2, 8.2},
rules.PrivateKey().RuleID: {10, 5.2, 8.2},
ruleConfig.PulumiAPIToken().RuleID: {10, 5.2, 8.2},
ruleConfig.PyPiUploadToken().RuleID: {10, 5.2, 8.2},
ruleConfig.RapidAPIAccessToken().RuleID: {10, 5.2, 8.2},
Expand Down

0 comments on commit 4bf7a43

Please sign in to comment.