[INS-397] Fix git version parser panic on non-numeric patch versions (#4882)

git built from source can report versions like "2.52.gaea8cc3", causing
an index out of range panic. The patch component is unused, so the regex
now captures only major.minor. Extract the helper into a shared pkg/gitcmd
package to remove duplication with the azureapimanagement detector.

Fixes #4801
This commit is contained in:
Shahzad Haider
2026-04-11 00:11:54 +05:00
committed by GitHub
parent afda467cf4
commit c120e8ccba
9 changed files with 137 additions and 77 deletions
@@ -3,10 +3,8 @@ package repositorykey
import (
"context"
"errors"
"fmt"
"net/url"
"os/exec"
"strconv"
"strings"
regexp "github.com/wasilibs/go-re2"
@@ -14,6 +12,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/gitcmd"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
)
@@ -100,36 +99,8 @@ func (s Scanner) Description() string {
return "Azure API Management Repository Keys provide access to the API Management (APIM) configuration repository, allowing users to directly interact with and modify API definitions, policies, and settings. These keys enable programmatic access to APIM's Git-based repository, where configurations can be cloned, edited, and pushed back to apply changes. They are primarily used for managing API configurations as code, automating deployments, and synchronizing APIM settings across environments."
}
func gitCmdCheck() error {
if errors.Is(exec.Command("git").Run(), exec.ErrNotFound) {
return fmt.Errorf("'git' command not found in $PATH. Make sure git is installed and included in $PATH")
}
// Check the version is greater than or equal to 2.20.0
out, err := exec.Command("git", "--version").Output()
if err != nil {
return fmt.Errorf("failed to check git version: %w", err)
}
// Extract the version string using a regex to find the version numbers
var regex = regexp.MustCompile(`\d+\.\d+\.\d+`)
versionStr := regex.FindString(string(out))
versionParts := strings.Split(versionStr, ".")
// Parse version numbers
major, _ := strconv.Atoi(versionParts[0])
minor, _ := strconv.Atoi(versionParts[1])
// Compare with version 2.20.0<=x<3.0.0
if major == 2 && minor >= 20 {
return nil
}
return fmt.Errorf("git version is %s, but must be greater than or equal to 2.20.0, and less than 3.0.0", versionStr)
}
func verifyUrlPassword(_ context.Context, repoUrl, user, password string) (bool, error) {
if err := gitCmdCheck(); err != nil {
if err := gitcmd.CheckVersion(); err != nil {
return false, err
}
+53
View File
@@ -0,0 +1,53 @@
// Package gitcmd provides helpers for interacting with the local git binary.
package gitcmd
import (
"fmt"
"os/exec"
"regexp"
"strconv"
"strings"
"github.com/go-errors/errors"
)
// gitVersionRegex captures the major and minor numbers from `git --version` output.
// We intentionally do not capture the patch version: git built from source can report
// a non-numeric patch like "git version 2.52.gaea8cc3", and the patch is unused here.
var gitVersionRegex = regexp.MustCompile(`(\d+)\.(\d+)`)
// CheckVersion checks if git is installed and meets 2.20.0<=x<3.0.0 version requirements.
func CheckVersion() error {
if errors.Is(exec.Command("git").Run(), exec.ErrNotFound) {
return fmt.Errorf("'git' command not found in $PATH. Make sure git is installed and included in $PATH")
}
// Check the version is greater than or equal to 2.20.0
out, err := exec.Command("git", "--version").Output()
if err != nil {
return fmt.Errorf("failed to check git version: %w", err)
}
major, minor, err := parseGitVersion(string(out))
if err != nil {
return err
}
// Compare with version 2.20.0<=x<3.0.0
if major == 2 && minor >= 20 {
return nil
}
return fmt.Errorf("git version is %d.%d, but must be greater than or equal to 2.20.0, and less than 3.0.0", major, minor)
}
// parseGitVersion extracts the major and minor numbers from `git --version` output.
func parseGitVersion(out string) (major, minor int, err error) {
matches := gitVersionRegex.FindStringSubmatch(out)
if len(matches) < 3 {
return 0, 0, fmt.Errorf("failed to parse git version from %q", strings.TrimSpace(out))
}
// Errors are impossible here since the regex only matches digits.
major, _ = strconv.Atoi(matches[1])
minor, _ = strconv.Atoi(matches[2])
return major, minor, nil
}
+71
View File
@@ -0,0 +1,71 @@
package gitcmd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseGitVersion(t *testing.T) {
tests := []struct {
name string
out string
wantMajor int
wantMinor int
wantErr bool
}{
{
name: "standard semver",
out: "git version 2.34.1\n",
wantMajor: 2,
wantMinor: 34,
},
{
name: "non-numeric patch (built from source)",
out: "git version 2.52.gaea8cc3\n",
wantMajor: 2,
wantMinor: 52,
},
{
name: "apple git suffix",
out: "git version 2.39.2 (Apple Git-143)\n",
wantMajor: 2,
wantMinor: 39,
},
{
name: "windows git suffix",
out: "git version 2.39.2.windows.1\n",
wantMajor: 2,
wantMinor: 39,
},
{
name: "no patch component",
out: "git version 2.20\n",
wantMajor: 2,
wantMinor: 20,
},
{
name: "no version present",
out: "git is not a version\n",
wantErr: true,
},
{
name: "empty output",
out: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
major, minor, err := parseGitVersion(tt.out)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.wantMajor, major)
assert.Equal(t, tt.wantMinor, minor)
})
}
}
-40
View File
@@ -1,40 +0,0 @@
package git
import (
"fmt"
"os/exec"
"regexp"
"strconv"
"strings"
"github.com/go-errors/errors"
)
// Extract the version string using a regex to find the version numbers
var regex = regexp.MustCompile(`\d+\.\d+\.\d+`)
// CmdCheck checks if git is installed and meets 2.20.0<=x<3.0.0 version requirements.
func CmdCheck() error {
if errors.Is(exec.Command("git").Run(), exec.ErrNotFound) {
return fmt.Errorf("'git' command not found in $PATH. Make sure git is installed and included in $PATH")
}
// Check the version is greater than or equal to 2.20.0
out, err := exec.Command("git", "--version").Output()
if err != nil {
return fmt.Errorf("failed to check git version: %w", err)
}
versionStr := regex.FindString(string(out))
versionParts := strings.Split(versionStr, ".")
// Parse version numbers
major, _ := strconv.Atoi(versionParts[0])
minor, _ := strconv.Atoi(versionParts[1])
// Compare with version 2.20.0<=x<3.0.0
if major == 2 && minor >= 20 {
return nil
}
return fmt.Errorf("git version is %s, but must be greater than or equal to 2.20.0, and less than 3.0.0", versionStr)
}
+3 -2
View File
@@ -30,6 +30,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
"github.com/trufflesecurity/trufflehog/v3/pkg/gitcmd"
"github.com/trufflesecurity/trufflehog/v3/pkg/gitparse"
"github.com/trufflesecurity/trufflehog/v3/pkg/handlers"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
@@ -214,7 +215,7 @@ func (s *Source) Init(aCtx context.Context, name string, jobId sources.JobID, so
concurrency = runtime.NumCPU()
}
if err = CmdCheck(); err != nil {
if err = gitcmd.CheckVersion(); err != nil {
return err
}
@@ -614,7 +615,7 @@ func executeClone(ctx context.Context, params cloneParams) (*git.Repository, err
//
// Pinging using other authentication methods is only unimplemented because there's been no pressing need for it yet.
func PingRepoUsingToken(ctx context.Context, token, gitUrl, user string) error {
if err := CmdCheck(); err != nil {
if err := gitcmd.CheckVersion(); err != nil {
return err
}
lsUrl, err := GitURLParse(gitUrl)
+2 -1
View File
@@ -28,6 +28,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
"github.com/trufflesecurity/trufflehog/v3/pkg/gitcmd"
"github.com/trufflesecurity/trufflehog/v3/pkg/giturl"
"github.com/trufflesecurity/trufflehog/v3/pkg/handlers"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
@@ -213,7 +214,7 @@ func (c *filteredRepoCache) wantRepo(s string) bool {
// Init returns an initialized GitHub source.
func (s *Source) Init(aCtx context.Context, name string, jobID sources.JobID, sourceID sources.SourceID, verify bool, connection *anypb.Any, concurrency int) error {
err := git.CmdCheck()
err := gitcmd.CheckVersion()
if err != nil {
return err
}
@@ -9,6 +9,7 @@ import (
"google.golang.org/protobuf/types/known/anypb"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/gitcmd"
"github.com/trufflesecurity/trufflehog/v3/pkg/giturl"
"github.com/trufflesecurity/trufflehog/v3/pkg/log"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
@@ -68,7 +69,7 @@ func (s *Source) JobID() sources.JobID {
// Init returns an initialized GitHubExperimental source.
func (s *Source) Init(aCtx context.Context, name string, jobID sources.JobID, sourceID sources.SourceID, verify bool, connection *anypb.Any, concurrency int) error {
err := git.CmdCheck()
err := gitcmd.CheckVersion()
if err != nil {
return err
}
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
"github.com/trufflesecurity/trufflehog/v3/pkg/gitcmd"
"github.com/trufflesecurity/trufflehog/v3/pkg/giturl"
"github.com/trufflesecurity/trufflehog/v3/pkg/log"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
@@ -166,7 +167,7 @@ func (s *Source) Init(ctx context.Context, name string, jobId sources.JobID, sou
s.jobPool = &errgroup.Group{}
s.jobPool.SetLimit(concurrency)
if err := git.CmdCheck(); err != nil {
if err := gitcmd.CheckVersion(); err != nil {
return err
}
+2 -1
View File
@@ -19,6 +19,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/gitcmd"
"github.com/trufflesecurity/trufflehog/v3/pkg/giturl"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
@@ -169,7 +170,7 @@ func (c *filteredRepoCache) includeRepo(s string) bool {
// Init returns an initialized HuggingFace source.
func (s *Source) Init(ctx context.Context, name string, jobID sources.JobID, sourceID sources.SourceID, verify bool, connection *anypb.Any, concurrency int) error {
err := git.CmdCheck()
err := gitcmd.CheckVersion()
if err != nil {
return err
}