Skip to content

Commit 55f55d8

Browse files
authored
Fix Analyze Local Requires Internet (#37)
* hiding git operations behind an interface and grouped the analysis dependencies under a struct for better management * implementing and using a local only git client for the analyze local
1 parent bb23d51 commit 55f55d8

File tree

3 files changed

+111
-38
lines changed

3 files changed

+111
-38
lines changed

analyze/analyze.go

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import (
99
"os"
1010
"strings"
1111
"sync"
12+
"time"
1213

1314
"github.com/rs/zerolog/log"
1415

1516
"github.com/boostsecurityio/poutine/opa"
16-
"github.com/boostsecurityio/poutine/providers/gitops"
1717
"github.com/boostsecurityio/poutine/providers/pkgsupply"
1818
"github.com/boostsecurityio/poutine/scanner"
1919
"github.com/schollz/progressbar/v3"
@@ -43,18 +43,40 @@ type ScmClient interface {
4343
ParseRepoAndOrg(string) (string, string, error)
4444
}
4545

46-
func AnalyzeOrg(ctx context.Context, org string, scmClient ScmClient, numberOfGoroutines *int, formatter Formatter) error {
47-
provider := scmClient.GetProviderName()
46+
type GitClient interface {
47+
Clone(ctx context.Context, clonePath string, url string, token string, ref string) error
48+
CommitSHA(clonePath string) (string, error)
49+
LastCommitDate(ctx context.Context, clonePath string) (time.Time, error)
50+
GetRemoteOriginURL(ctx context.Context, repoPath string) (string, error)
51+
GetRepoHeadBranchName(ctx context.Context, repoPath string) (string, error)
52+
}
53+
54+
func NewAnalyzer(scmClient ScmClient, gitClient GitClient, formatter Formatter) *Analyzer {
55+
return &Analyzer{
56+
ScmClient: scmClient,
57+
GitClient: gitClient,
58+
Formatter: formatter,
59+
}
60+
}
61+
62+
type Analyzer struct {
63+
ScmClient ScmClient
64+
GitClient GitClient
65+
Formatter Formatter
66+
}
67+
68+
func (a *Analyzer) AnalyzeOrg(ctx context.Context, org string, numberOfGoroutines *int) error {
69+
provider := a.ScmClient.GetProviderName()
4870

49-
providerVersion, err := scmClient.GetProviderVersion(ctx)
71+
providerVersion, err := a.ScmClient.GetProviderVersion(ctx)
5072
if err != nil {
5173
log.Debug().Err(err).Msgf("Failed to get provider version for %s", provider)
5274
}
5375

5476
log.Debug().Msgf("Provider: %s, Version: %s", provider, providerVersion)
5577

5678
log.Debug().Msgf("Fetching list of repositories for organization: %s on %s", org, provider)
57-
orgReposBatches := scmClient.GetOrgRepos(ctx, org)
79+
orgReposBatches := a.ScmClient.GetOrgRepos(ctx, org)
5880

5981
opaClient, _ := opa.NewOpa()
6082
pkgsupplyClient := pkgsupply.NewStaticClient()
@@ -96,14 +118,14 @@ func AnalyzeOrg(ctx context.Context, org string, scmClient ScmClient, numberOfGo
96118
defer sem.Release(1)
97119
defer wg.Done()
98120
repoNameWithOwner := repo.GetRepoIdentifier()
99-
tempDir, err := cloneRepoToTemp(ctx, repo.BuildGitURL(scmClient.GetProviderBaseURL()), scmClient.GetToken())
121+
tempDir, err := a.cloneRepoToTemp(ctx, repo.BuildGitURL(a.ScmClient.GetProviderBaseURL()), a.ScmClient.GetToken())
100122
if err != nil {
101123
log.Error().Err(err).Str("repo", repoNameWithOwner).Msg("failed to clone repo")
102124
return
103125
}
104126
defer os.RemoveAll(tempDir)
105127

106-
pkg, err := generatePackageInsights(ctx, tempDir, repo)
128+
pkg, err := a.generatePackageInsights(ctx, tempDir, repo)
107129
if err != nil {
108130
errChan <- err
109131
return
@@ -132,21 +154,21 @@ func AnalyzeOrg(ctx context.Context, org string, scmClient ScmClient, numberOfGo
132154

133155
fmt.Print("\n\n")
134156

135-
return finalizeAnalysis(ctx, inventory, formatter)
157+
return a.finalizeAnalysis(ctx, inventory)
136158
}
137159

138-
func AnalyzeRepo(ctx context.Context, repoString string, scmClient ScmClient, formatter Formatter) error {
139-
org, repoName, err := scmClient.ParseRepoAndOrg(repoString)
160+
func (a *Analyzer) AnalyzeRepo(ctx context.Context, repoString string) error {
161+
org, repoName, err := a.ScmClient.ParseRepoAndOrg(repoString)
140162
if err != nil {
141163
return fmt.Errorf("failed to parse repository: %w", err)
142164
}
143-
repo, err := scmClient.GetRepo(ctx, org, repoName)
165+
repo, err := a.ScmClient.GetRepo(ctx, org, repoName)
144166
if err != nil {
145167
return fmt.Errorf("failed to get repo: %w", err)
146168
}
147169
provider := repo.GetProviderName()
148170

149-
providerVersion, err := scmClient.GetProviderVersion(ctx)
171+
providerVersion, err := a.ScmClient.GetProviderVersion(ctx)
150172
if err != nil {
151173
log.Debug().Err(err).Msgf("Failed to get provider version for %s", provider)
152174
}
@@ -166,13 +188,13 @@ func AnalyzeRepo(ctx context.Context, repoString string, scmClient ScmClient, fo
166188
progressbar.OptionSetWriter(os.Stderr),
167189
)
168190

169-
tempDir, err := cloneRepoToTemp(ctx, repo.BuildGitURL(scmClient.GetProviderBaseURL()), scmClient.GetToken())
191+
tempDir, err := a.cloneRepoToTemp(ctx, repo.BuildGitURL(a.ScmClient.GetProviderBaseURL()), a.ScmClient.GetToken())
170192
if err != nil {
171193
return err
172194
}
173195
defer os.RemoveAll(tempDir)
174196

175-
pkg, err := generatePackageInsights(ctx, tempDir, repo)
197+
pkg, err := a.generatePackageInsights(ctx, tempDir, repo)
176198
if err != nil {
177199
return err
178200
}
@@ -184,21 +206,21 @@ func AnalyzeRepo(ctx context.Context, repoString string, scmClient ScmClient, fo
184206
_ = bar.Add(1)
185207

186208
fmt.Print("\n\n")
187-
return finalizeAnalysis(ctx, inventory, formatter)
209+
return a.finalizeAnalysis(ctx, inventory)
188210
}
189211

190-
func AnalyzeLocalRepo(ctx context.Context, repoPath string, scmClient ScmClient, formatter Formatter) error {
191-
org, repoName, err := scmClient.ParseRepoAndOrg(repoPath)
212+
func (a *Analyzer) AnalyzeLocalRepo(ctx context.Context, repoPath string) error {
213+
org, repoName, err := a.ScmClient.ParseRepoAndOrg(repoPath)
192214
if err != nil {
193215
return fmt.Errorf("failed to parse repository: %w", err)
194216
}
195-
repo, err := scmClient.GetRepo(ctx, org, repoName)
217+
repo, err := a.ScmClient.GetRepo(ctx, org, repoName)
196218
if err != nil {
197219
return fmt.Errorf("failed to get repo: %w", err)
198220
}
199221
provider := repo.GetProviderName()
200222

201-
providerVersion, err := scmClient.GetProviderVersion(ctx)
223+
providerVersion, err := a.ScmClient.GetProviderVersion(ctx)
202224
if err != nil {
203225
log.Debug().Err(err).Msgf("Failed to get provider version for %s", provider)
204226
}
@@ -218,7 +240,7 @@ func AnalyzeLocalRepo(ctx context.Context, repoPath string, scmClient ScmClient,
218240
progressbar.OptionSetWriter(os.Stderr),
219241
)
220242

221-
pkg, err := generatePackageInsights(ctx, repoPath, repo)
243+
pkg, err := a.generatePackageInsights(ctx, repoPath, repo)
222244
if err != nil {
223245
return err
224246
}
@@ -230,40 +252,39 @@ func AnalyzeLocalRepo(ctx context.Context, repoPath string, scmClient ScmClient,
230252
_ = bar.Add(1)
231253

232254
fmt.Print("\n\n")
233-
return finalizeAnalysis(ctx, inventory, formatter)
255+
return a.finalizeAnalysis(ctx, inventory)
234256
}
235257

236258
type Formatter interface {
237259
Format(ctx context.Context, report *opa.FindingsResult, packages []*models.PackageInsights) error
238260
}
239261

240-
func finalizeAnalysis(ctx context.Context, inventory *scanner.Inventory, formatter Formatter) error {
262+
func (a *Analyzer) finalizeAnalysis(ctx context.Context, inventory *scanner.Inventory) error {
241263
report, err := inventory.Findings(ctx)
242264
if err != nil {
243265
return err
244266
}
245267

246-
err = formatter.Format(ctx, report, inventory.Packages)
268+
err = a.Formatter.Format(ctx, report, inventory.Packages)
247269
if err != nil {
248270
return err
249271
}
250272

251273
return nil
252274
}
253275

254-
func generatePackageInsights(ctx context.Context, tempDir string, repo Repository) (*models.PackageInsights, error) {
255-
gitClient := gitops.NewGitClient(nil)
256-
commitDate, err := gitClient.LastCommitDate(ctx, tempDir)
276+
func (a *Analyzer) generatePackageInsights(ctx context.Context, tempDir string, repo Repository) (*models.PackageInsights, error) {
277+
commitDate, err := a.GitClient.LastCommitDate(ctx, tempDir)
257278
if err != nil {
258279
return nil, fmt.Errorf("failed to get last commit date: %w", err)
259280
}
260281

261-
commitSha, err := gitClient.CommitSHA(tempDir)
282+
commitSha, err := a.GitClient.CommitSHA(tempDir)
262283
if err != nil {
263284
return nil, fmt.Errorf("failed to get commit SHA: %w", err)
264285
}
265286

266-
headBranchName, err := gitClient.GetRepoHeadBranchName(ctx, tempDir)
287+
headBranchName, err := a.GitClient.GetRepoHeadBranchName(ctx, tempDir)
267288
if err != nil {
268289
return nil, fmt.Errorf("failed to get head branch name: %w", err)
269290
}
@@ -284,14 +305,13 @@ func generatePackageInsights(ctx context.Context, tempDir string, repo Repositor
284305
return pkg, nil
285306
}
286307

287-
func cloneRepoToTemp(ctx context.Context, gitURL string, token string) (string, error) {
308+
func (a *Analyzer) cloneRepoToTemp(ctx context.Context, gitURL string, token string) (string, error) {
288309
tempDir, err := os.MkdirTemp("", TEMP_DIR_PREFIX)
289310
if err != nil {
290311
return "", fmt.Errorf("failed to create temp directory: %w", err)
291312
}
292313

293-
gitClient := gitops.NewGitClient(nil)
294-
err = gitClient.Clone(ctx, tempDir, gitURL, token, "HEAD")
314+
err = a.GitClient.Clone(ctx, tempDir, gitURL, token, "HEAD")
295315
if err != nil {
296316
os.RemoveAll(tempDir) // Clean up if cloning fails
297317
return "", fmt.Errorf("failed to clone repo: %s", err)

poutine.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"flag"
66
"fmt"
7+
"github.com/boostsecurityio/poutine/providers/gitops"
78
"os"
89
"os/signal"
910
"path/filepath"
@@ -113,33 +114,37 @@ func run(ctx context.Context, args []string) error {
113114

114115
formatter := getFormatter()
115116

117+
gitClient := gitops.NewGitClient(nil)
118+
119+
analyzer := analyze.NewAnalyzer(scmClient, gitClient, formatter)
120+
116121
switch command {
117122
case "analyze_org":
118-
return analyzeOrg(ctx, args[1], scmClient, formatter)
123+
return analyzeOrg(ctx, args[1], analyzer)
119124
case "analyze_repo":
120-
return analyzeRepo(ctx, args[1], scmClient, formatter)
125+
return analyzeRepo(ctx, args[1], analyzer)
121126
case "analyze_local":
122127
return analyzeLocal(ctx, args[1], formatter)
123128
default:
124129
return fmt.Errorf("unknown command %q", command)
125130
}
126131
}
127132

128-
func analyzeOrg(ctx context.Context, org string, scmClient analyze.ScmClient, formatter analyze.Formatter) error {
133+
func analyzeOrg(ctx context.Context, org string, analyzer *analyze.Analyzer) error {
129134
if org == "" {
130135
return fmt.Errorf("invalid organization name %q", org)
131136
}
132137

133-
err := analyze.AnalyzeOrg(ctx, org, scmClient, threads, formatter)
138+
err := analyzer.AnalyzeOrg(ctx, org, threads)
134139
if err != nil {
135140
return fmt.Errorf("failed to analyze org %s: %w", org, err)
136141
}
137142

138143
return nil
139144
}
140145

141-
func analyzeRepo(ctx context.Context, repo string, scmClient analyze.ScmClient, formatter analyze.Formatter) error {
142-
err := analyze.AnalyzeRepo(ctx, repo, scmClient, formatter)
146+
func analyzeRepo(ctx context.Context, repo string, analyzer *analyze.Analyzer) error {
147+
err := analyzer.AnalyzeRepo(ctx, repo)
143148
if err != nil {
144149
return fmt.Errorf("failed to analyze repo %s: %w", repo, err)
145150
}
@@ -152,7 +157,12 @@ func analyzeLocal(ctx context.Context, repoPath string, formatter analyze.Format
152157
if err != nil {
153158
return fmt.Errorf("failed to create local SCM client: %w", err)
154159
}
155-
err = analyze.AnalyzeLocalRepo(ctx, repoPath, localScmClient, formatter)
160+
161+
localGitClient := gitops.NewLocalGitClient(nil)
162+
163+
analyzer := analyze.NewAnalyzer(localScmClient, localGitClient, formatter)
164+
165+
err = analyzer.AnalyzeLocalRepo(ctx, repoPath)
156166
if err != nil {
157167
return fmt.Errorf("failed to analyze repoPath %s: %w", repoPath, err)
158168
}

providers/gitops/gitops.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package gitops
33
import (
44
"bytes"
55
"context"
6+
"github.com/rs/zerolog/log"
67
"os"
78
"os/exec"
89
"strconv"
@@ -132,3 +133,45 @@ func (g *GitClient) GetRepoHeadBranchName(ctx context.Context, repoPath string)
132133

133134
return "HEAD", nil
134135
}
136+
137+
func NewLocalGitClient(command *GitCommand) *LocalGitClient {
138+
if command != nil {
139+
return &LocalGitClient{GitClient: &GitClient{Command: *command}}
140+
}
141+
return &LocalGitClient{GitClient: &GitClient{Command: &ExecGitCommand{}}}
142+
}
143+
144+
type LocalGitClient struct {
145+
GitClient *GitClient
146+
}
147+
148+
func (g *LocalGitClient) GetRemoteOriginURL(ctx context.Context, repoPath string) (string, error) {
149+
return g.GitClient.GetRemoteOriginURL(ctx, repoPath)
150+
}
151+
152+
func (g *LocalGitClient) LastCommitDate(ctx context.Context, clonePath string) (time.Time, error) {
153+
return g.GitClient.LastCommitDate(ctx, clonePath)
154+
}
155+
156+
func (g *LocalGitClient) CommitSHA(clonePath string) (string, error) {
157+
return g.GitClient.CommitSHA(clonePath)
158+
}
159+
160+
func (g *LocalGitClient) Clone(ctx context.Context, clonePath string, url string, token string, ref string) error {
161+
log.Debug().Msgf("Local Git Client shouldn't be used to clone repositories")
162+
return nil
163+
}
164+
165+
func (g *LocalGitClient) GetRepoHeadBranchName(ctx context.Context, repoPath string) (string, error) {
166+
cmd := "git"
167+
args := []string{"branch", "--show-current"}
168+
169+
output, err := g.GitClient.Command.Run(ctx, cmd, args, repoPath)
170+
if err != nil {
171+
return "", err
172+
}
173+
174+
headBranch := string(bytes.TrimSpace(output))
175+
176+
return headBranch, nil
177+
}

0 commit comments

Comments
 (0)