@@ -5,15 +5,19 @@ import (
5
5
"context"
6
6
"fmt"
7
7
"os"
8
+ "path/filepath"
9
+ "regexp"
10
+ "strings"
8
11
"sync"
9
12
"time"
10
13
11
14
"github.com/boostsecurityio/poutine/models"
15
+ "github.com/boostsecurityio/poutine/results"
12
16
"golang.org/x/sync/semaphore"
13
17
14
18
"github.com/boostsecurityio/poutine/opa"
15
19
"github.com/boostsecurityio/poutine/providers/pkgsupply"
16
- "github.com/boostsecurityio/poutine/providers/scm/domain"
20
+ scm_domain "github.com/boostsecurityio/poutine/providers/scm/domain"
17
21
"github.com/boostsecurityio/poutine/scanner"
18
22
"github.com/rs/zerolog/log"
19
23
"github.com/schollz/progressbar/v3"
@@ -60,10 +64,13 @@ type ScmClient interface {
60
64
61
65
type GitClient interface {
62
66
Clone (ctx context.Context , clonePath string , url string , token string , ref string ) error
67
+ FetchCone (ctx context.Context , clonePath string , url string , token string , ref string , cone string ) error
63
68
CommitSHA (clonePath string ) (string , error )
64
69
LastCommitDate (ctx context.Context , clonePath string ) (time.Time , error )
65
70
GetRemoteOriginURL (ctx context.Context , repoPath string ) (string , error )
66
71
GetRepoHeadBranchName (ctx context.Context , repoPath string ) (string , error )
72
+ GetUniqWorkflowsBranches (ctx context.Context , clonePath string ) (map [string ][]models.BranchInfo , error )
73
+ BlobMatches (ctx context.Context , clonePath string , blobsha string , regex * regexp.Regexp ) (bool , []byte , error )
67
74
}
68
75
69
76
func NewAnalyzer (scmClient ScmClient , gitClient GitClient , formatter Formatter , config * models.Config , opaClient * opa.Opa ) * Analyzer {
@@ -208,6 +215,153 @@ func (a *Analyzer) AnalyzeOrg(ctx context.Context, org string, numberOfGoroutine
208
215
return scannedPackages , nil
209
216
}
210
217
218
+ func (a * Analyzer ) AnalyzeStaleBranches (ctx context.Context , repoString string , numberOfGoroutines * int , expand * bool , regex * regexp.Regexp ) (* models.PackageInsights , error ) {
219
+ org , repoName , err := a .ScmClient .ParseRepoAndOrg (repoString )
220
+ if err != nil {
221
+ return nil , fmt .Errorf ("failed to parse repository: %w" , err )
222
+ }
223
+ repo , err := a .ScmClient .GetRepo (ctx , org , repoName )
224
+ if err != nil {
225
+ return nil , fmt .Errorf ("failed to get repo: %w" , err )
226
+ }
227
+ provider := repo .GetProviderName ()
228
+
229
+ providerVersion , err := a .ScmClient .GetProviderVersion (ctx )
230
+ if err != nil {
231
+ log .Debug ().Err (err ).Msgf ("Failed to get provider version for %s" , provider )
232
+ }
233
+
234
+ log .Debug ().Msgf ("Provider: %s, Version: %s, BaseURL: %s" , provider , providerVersion , a .ScmClient .GetProviderBaseURL ())
235
+
236
+ pkgsupplyClient := pkgsupply .NewStaticClient ()
237
+
238
+ inventory := scanner .NewInventory (a .Opa , pkgsupplyClient , provider , providerVersion )
239
+
240
+ log .Debug ().Msgf ("Starting repository analysis for: %s/%s on %s" , org , repoName , provider )
241
+ bar := a .progressBar (3 , "Cloning repository" )
242
+ _ = bar .RenderBlank ()
243
+
244
+ repoUrl := repo .BuildGitURL (a .ScmClient .GetProviderBaseURL ())
245
+ tempDir , err := a .fetchConeToTemp (ctx , repoUrl , a .ScmClient .GetToken (), "refs/heads/*:refs/remotes/origin/*" , ".github/workflows" )
246
+ if err != nil {
247
+ return nil , fmt .Errorf ("failed to fetch cone: %w" , err )
248
+ }
249
+ defer os .RemoveAll (tempDir )
250
+
251
+ bar .Describe ("Listing unique workflows" )
252
+ _ = bar .Add (1 )
253
+
254
+ workflows , err := a .GitClient .GetUniqWorkflowsBranches (ctx , tempDir )
255
+ if err != nil {
256
+ return nil , fmt .Errorf ("failed to get unique workflow: %w" , err )
257
+ }
258
+
259
+ bar .Describe ("Check which workflows match regex: " + regex .String ())
260
+ _ = bar .Add (1 )
261
+
262
+ workflowDir := filepath .Join (tempDir , ".github" , "workflows" )
263
+ if err = os .MkdirAll (workflowDir , 0700 ); err != nil {
264
+ return nil , fmt .Errorf ("failed to create .github/workflows/ dir: %w" , err )
265
+ }
266
+
267
+ wg := sync.WaitGroup {}
268
+ errChan := make (chan error , 1 )
269
+ maxGoroutines := 5
270
+ if numberOfGoroutines != nil {
271
+ maxGoroutines = * numberOfGoroutines
272
+ }
273
+ semaphore := semaphore .NewWeighted (int64 (maxGoroutines ))
274
+ m := sync.Mutex {}
275
+ blobShas := make ([]string , 0 , len (workflows ))
276
+ for sha := range workflows {
277
+ blobShas = append (blobShas , sha )
278
+ }
279
+ for _ , blobSha := range blobShas {
280
+ if err := semaphore .Acquire (ctx , 1 ); err != nil {
281
+ errChan <- fmt .Errorf ("failed to acquire semaphore: %w" , err )
282
+ break
283
+ }
284
+ wg .Add (1 )
285
+ go func (blobSha string ) {
286
+ defer wg .Done ()
287
+ defer semaphore .Release (1 )
288
+ match , content , err := a .GitClient .BlobMatches (ctx , tempDir , blobSha , regex )
289
+ if err != nil {
290
+ errChan <- fmt .Errorf ("failed to blob match %s: %w" , blobSha , err )
291
+ return
292
+ }
293
+ if match {
294
+ err = os .WriteFile (filepath .Join (workflowDir , blobSha + ".yaml" ), content , 0644 )
295
+ if err != nil {
296
+ errChan <- fmt .Errorf ("failed to write file for blob %s: %w" , blobSha , err )
297
+ }
298
+ } else {
299
+ m .Lock ()
300
+ delete (workflows , blobSha )
301
+ m .Unlock ()
302
+ }
303
+ }(blobSha )
304
+ }
305
+ wg .Wait ()
306
+ close (errChan )
307
+ for err := range errChan {
308
+ return nil , err
309
+ }
310
+
311
+ bar .Describe ("Scanning package" )
312
+ _ = bar .Add (1 )
313
+ pkg , err := a .generatePackageInsights (ctx , tempDir , repo , "HEAD" )
314
+ if err != nil {
315
+ return nil , fmt .Errorf ("failed to generate package insight: %w" , err )
316
+ }
317
+
318
+ inventoryScanner := scanner.InventoryScanner {
319
+ Path : tempDir ,
320
+ Parsers : []scanner.Parser {
321
+ scanner .NewGithubActionWorkflowParser (),
322
+ },
323
+ }
324
+
325
+ scannedPackage , err := inventory .ScanPackageScanner (ctx , * pkg , & inventoryScanner )
326
+ if err != nil {
327
+ return nil , fmt .Errorf ("failed to scan package: %w" , err )
328
+ }
329
+
330
+ _ = bar .Finish ()
331
+ if * expand {
332
+ expanded := []results.Finding {}
333
+ for _ , finding := range scannedPackage .FindingsResults .Findings {
334
+ filename := filepath .Base (finding .Meta .Path )
335
+ blobsha := strings .TrimSuffix (filename , filepath .Ext (filename ))
336
+ purl , err := models .NewPurl (finding .Purl )
337
+ if err != nil {
338
+ log .Warn ().Err (err ).Str ("purl" , finding .Purl ).Msg ("failed to evaluate PURL, skipping" )
339
+ continue
340
+ }
341
+ for _ , branchInfo := range workflows [blobsha ] {
342
+ for _ , path := range branchInfo .FilePath {
343
+ finding .Meta .Path = path
344
+ purl .Version = branchInfo .BranchName
345
+ finding .Purl = purl .String ()
346
+ expanded = append (expanded , finding )
347
+ }
348
+ }
349
+ }
350
+ scannedPackage .FindingsResults .Findings = expanded
351
+
352
+ if err := a .Formatter .Format (ctx , []* models.PackageInsights {scannedPackage }); err != nil {
353
+ return nil , fmt .Errorf ("failed to finalize analysis of package: %w" , err )
354
+ }
355
+ } else {
356
+ if err := a .Formatter .FormatWithPath (ctx , []* models.PackageInsights {scannedPackage }, workflows ); err != nil {
357
+ return nil , fmt .Errorf ("failed to finalize analysis of package: %w" , err )
358
+ }
359
+
360
+ }
361
+
362
+ return scannedPackage , nil
363
+ }
364
+
211
365
func (a * Analyzer ) AnalyzeRepo (ctx context.Context , repoString string , ref string ) (* models.PackageInsights , error ) {
212
366
org , repoName , err := a .ScmClient .ParseRepoAndOrg (repoString )
213
367
if err != nil {
@@ -304,6 +458,7 @@ func (a *Analyzer) AnalyzeLocalRepo(ctx context.Context, repoPath string) (*mode
304
458
305
459
type Formatter interface {
306
460
Format (ctx context.Context , packages []* models.PackageInsights ) error
461
+ FormatWithPath (ctx context.Context , packages []* models.PackageInsights , pathAssociation map [string ][]models.BranchInfo ) error
307
462
}
308
463
309
464
func (a * Analyzer ) finalizeAnalysis (ctx context.Context , scannedPackages []* models.PackageInsights ) error {
@@ -316,14 +471,15 @@ func (a *Analyzer) finalizeAnalysis(ctx context.Context, scannedPackages []*mode
316
471
}
317
472
318
473
func (a * Analyzer ) generatePackageInsights (ctx context.Context , tempDir string , repo Repository , ref string ) (* models.PackageInsights , error ) {
474
+ var err error
319
475
commitDate , err := a .GitClient .LastCommitDate (ctx , tempDir )
320
476
if err != nil {
321
- return nil , fmt . Errorf ( "failed to get last commit date: %w" , err )
477
+ log . Ctx ( ctx ). Warn (). Err ( err ). Msg ( "failed to get last commit date" )
322
478
}
323
479
324
480
commitSha , err := a .GitClient .CommitSHA (tempDir )
325
481
if err != nil {
326
- return nil , fmt . Errorf ( "failed to get commit SHA: %w" , err )
482
+ log . Ctx ( ctx ). Warn (). Err ( err ). Msg ( "failed to get commit SHA" )
327
483
}
328
484
329
485
var (
@@ -376,6 +532,20 @@ func (a *Analyzer) generatePackageInsights(ctx context.Context, tempDir string,
376
532
return pkg , nil
377
533
}
378
534
535
+ func (a * Analyzer ) fetchConeToTemp (ctx context.Context , gitURL , token , ref string , cone string ) (string , error ) {
536
+ tempDir , err := os .MkdirTemp ("" , TEMP_DIR_PREFIX )
537
+ if err != nil {
538
+ return "" , fmt .Errorf ("failed to create temp directory: %w" , err )
539
+ }
540
+
541
+ err = a .GitClient .FetchCone (ctx , tempDir , gitURL , token , ref , cone )
542
+ if err != nil {
543
+ os .RemoveAll (tempDir ) // Clean up if cloning fails
544
+ return "" , fmt .Errorf ("failed to clone repo: %w" , err )
545
+ }
546
+ return tempDir , nil
547
+ }
548
+
379
549
func (a * Analyzer ) cloneRepoToTemp (ctx context.Context , gitURL string , token string , ref string ) (string , error ) {
380
550
tempDir , err := os .MkdirTemp ("" , TEMP_DIR_PREFIX )
381
551
if err != nil {
0 commit comments