Repository: safedep/vet
Pull request #495 (2025-05-27 15:00:42)

refactor: Sync reporter to allow env resolver adapter

github-actions[bot]: # vet Summary Report
This report is generated by [vet](https://github.com/safedep/vet)

## Policy Checks
- :white_check_mark: Vulnerability
- :white_check_mark: Malware
- :white_check_mark: License
- :white_check_mark: Popularity
- :white_check_mark: Maintenance
- :white_check_mark: Security Posture
- :white_check_mark: Threats
## Malicious Package Analysis
Malicious package analysis was performed using [SafeDep Cloud API](https://docs.safedep.io/cloud/malware-analysis)

<details>
<summary>Malicious Package Analysis Report</summary>


| Ecosystem | Package | Version | Status | Report |
| --- | --- | --- | --- | --- |
| ECOSYSTEM_GO | buf.build/gen/go/safedep/api/grpc/go | 1.5.1-20250527100058-ba6815156f1f.2 | :white_check_mark: | [:link:](https://platform.safedep.io/community/malysis/01JW8MTC5ED07NY8CH1PA2CZDT) |
| ECOSYSTEM_GO | buf.build/gen/go/safedep/api/protocolbuffers/go | 1.36.6-20250527100058-ba6815156f1f.1 | :white_check_mark: | [:link:](https://platform.safedep.io/community/malysis/01JW8MTC85E3YDHSCC6WNN43Y5) |


</details>

- :information_source: 2 packages have been actively analyzed for malicious behaviour.
- :white_check_mark: No malicious packages found.
<details>
<summary>Changed Packages</summary>

## Changed Packages
- :white_check_mark: [`Go`] `buf.build/gen/go/safedep/api/protocolbuffers/go@1.36.6-20250527100058-ba6815156f1f.1`
- :white_check_mark: [`Go`] `buf.build/gen/go/safedep/api/grpc/go@1.5.1-20250527100058-ba6815156f1f.2`
</details>



<!-- vet-report-pr-comment -->
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/safedep/vet/pull/495?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=safedep) Report
Attention: Patch coverage is `60.37736%` with `21 lines` in your changes missing coverage. Please review.
> Project coverage is 16.41%. Comparing base [(`826b8ea`)](https://app.codecov.io/gh/safedep/vet/commit/826b8eafdf436b907f149dd3a1fd3d9831eb5469?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=safedep) to head [(`3fd5d5c`)](https://app.codecov.io/gh/safedep/vet/commit/3fd5d5c0b1b707d71074616b4d670760f217ccbc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=safedep).

| [Files with missing lines](https://app.codecov.io/gh/safedep/vet/pull/495?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=safedep) | Patch % | Lines |
|---|---|---|
| [pkg/reporter/sync.go](https://app.codecov.io/gh/safedep/vet/pull/495?src=pr&el=tree&filepath=pkg%2Freporter%2Fsync.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=safedep#diff-cGtnL3JlcG9ydGVyL3N5bmMuZ28=) | 61.53% | [20 Missing :warning: ](https://app.codecov.io/gh/safedep/vet/pull/495?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=safedep) |
| [scan.go](https://app.codecov.io/gh/safedep/vet/pull/495?src=pr&el=tree&filepath=scan.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=safedep#diff-c2Nhbi5nbw==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/safedep/vet/pull/495?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=safedep) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #495      +/-   ##
==========================================
+ Coverage   16.23%   16.41%   +0.17%     
==========================================
  Files         160      160              
  Lines       16024    16044      +20     
==========================================
+ Hits         2602     2634      +32     
+ Misses      13218    13206      -12     
  Partials      204      204              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/safedep/vet/pull/495?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=safedep).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=safedep).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/go.mod b/go.mod
index ce7400e..4a97d45 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,8 @@ module github.com/safedep/vet
 go 1.24.2
 
 require (
-	buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250418165058-162f6b0cc319.2
-	buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250418165058-162f6b0cc319.1
+	buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250527100058-ba6815156f1f.2
+	buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250527100058-ba6815156f1f.1
 	entgo.io/ent v0.14.4
 	github.com/AlecAivazis/survey/v2 v2.3.7
 	github.com/BurntSushi/toml v1.5.0
diff --git a/go.sum b/go.sum
index 98f7d55..a19eab8 100644
--- a/go.sum
+++ b/go.sum
@@ -8,8 +8,12 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-2025030720450
 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250307204501-0409229c3780.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
 buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250418165058-162f6b0cc319.2 h1:txDywYkqsXvtA/sDSMcwMjC8XTHnqlyc+3aIFMzRVeQ=
 buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250418165058-162f6b0cc319.2/go.mod h1:fdc8Y27iNGRno4W5CMBFBOQlG75NJ3M9q0GZRvY/TVA=
+buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250527100058-ba6815156f1f.2 h1:WfzhSnTtAbi5Zn9GwgNkhPDTsQD8ismpUukHrxvPGLQ=
+buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250527100058-ba6815156f1f.2/go.mod h1:HNLXNUIlZB8rXnvWj0hVVze1ioWtUa+GauXoM85qpkI=
 buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250418165058-162f6b0cc319.1 h1:vJeI1IQuxGZd9r5RHNiLTJ+aPQfy0g7h3Gqa9/Ql7kA=
 buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250418165058-162f6b0cc319.1/go.mod h1:uR95GqsnNCRn6cTyRBte6uMJMm0rEBRxTGpakKCNL9I=
+buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250527100058-ba6815156f1f.1 h1:Fv2LmHer2zT/JBuxQEi336gMmG62lbRnCcNuHhg0MvA=
+buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250527100058-ba6815156f1f.1/go.mod h1:uR95GqsnNCRn6cTyRBte6uMJMm0rEBRxTGpakKCNL9I=
 cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
 cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
diff --git a/pkg/reporter/sync.go b/pkg/reporter/sync.go
index 1d6ce83..cefb852 100644
--- a/pkg/reporter/sync.go
+++ b/pkg/reporter/sync.go
@@ -7,6 +7,7 @@ import (
 	"sync"
 
 	"buf.build/gen/go/safedep/api/grpc/go/safedep/services/controltower/v1/controltowerv1grpc"
+	controltowerv1pb "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/controltower/v1"
 	packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
 	policyv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/policy/v1"
 	vulnerabilityv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/vulnerability/v1"
@@ -23,6 +24,55 @@ import (
 
 const syncReporterDefaultWorkerCount = 10
 
+// Contract for implementing environment resolver for the sync reporter.
+// Here we decouple the the actual implementation of the resolver to the
+// client that uses the reporter plugin. The resolver is used to provide
+// environment awareness to the reporter. For example, when running in GitHub
+// or on a Git repository, the resolver can provide the project source, project
+// and other information that is required to create a tool session.
+type SyncReporterEnvResolver interface {
+	// The resolved source of the runtime environment (e.g. GitHub)
+	GetProjectSource() controltowerv1pb.Project_Source
+
+	// The resolved URL of the runtime environment (e.g. GitHub repository URL)
+	GetProjectUrl() string
+
+	// The trigger of the runtime environment (e.g. CI/CD pipeline)
+	Trigger() controltowerv1.ToolTrigger
+
+	// The Git reference of the runtime environment (e.g. branch, tag, commit)
+	GitRef() string
+
+	// The Git SHA of the runtime environment (e.g. commit hash)
+	GitSha() string
+}
+
+type defaultSyncReporterEnvResolver struct{}
+
+func (r *defaultSyncReporterEnvResolver) GetProjectSource() controltowerv1pb.Project_Source {
+	return controltowerv1pb.Project_SOURCE_UNSPECIFIED
+}
+
+func (r *defaultSyncReporterEnvResolver) GetProjectUrl() string {
+	return ""
+}
+
+func (r *defaultSyncReporterEnvResolver) GitRef() string {
+	return ""
+}
+
+func (r *defaultSyncReporterEnvResolver) GitSha() string {
+	return ""
+}
+
+func (r *defaultSyncReporterEnvResolver) Trigger() controltowerv1.ToolTrigger {
+	return controltowerv1.ToolTrigger_TOOL_TRIGGER_MANUAL
+}
+
+func DefaultSyncReporterEnvResolver() SyncReporterEnvResolver {
+	return &defaultSyncReporterEnvResolver{}
+}
+
 type SyncReporterConfig struct {
 	// gRPC connection for ControlTower
 	ClientConnection *grpc.ClientConn
@@ -31,16 +81,9 @@ type SyncReporterConfig struct {
 	// In this case, a new project is created per package manifest
 	EnableMultiProjectSync bool
 
-	// Required
+	// Required when scanning a single project
 	ProjectName    string
 	ProjectVersion string
-	TriggerEvent   string
-
-	// Optional or auto-discovered from environment
-	GitRef     string
-	GitRefName string
-	GitRefType string
-	GitSha     string
 
 	// Performance
 	WorkerCount int
@@ -123,32 +166,42 @@ type workItem struct {
 }
 
 type syncReporter struct {
-	config    *SyncReporterConfig
-	workQueue chan *workItem
-	done      chan bool
-	wg        sync.WaitGroup
-	client    *grpc.ClientConn
-	sessions  *syncSessionPool
-	callbacks SyncReporterCallbacks
+	config      *SyncReporterConfig
+	workQueue   chan *workItem
+	done        chan bool
+	wg          sync.WaitGroup
+	client      *grpc.ClientConn
+	sessions    *syncSessionPool
+	envResolver SyncReporterEnvResolver
+	callbacks   SyncReporterCallbacks
 }
 
 // Verify syncReporter implements the Reporter interface
 var _ Reporter = (*syncReporter)(nil)
 
-func NewSyncReporter(config SyncReporterConfig, callbacks SyncReporterCallbacks) (Reporter, error) {
+func NewSyncReporter(config SyncReporterConfig, envResolver SyncReporterEnvResolver, callbacks SyncReporterCallbacks) (*syncReporter, error) {
 	if config.ClientConnection == nil {
 		return nil, fmt.Errorf("missing gRPC client connection")
 	}
 
-	// TODO: Auto-discover config using CI environment variables
-	// if enabled by the user
+	if envResolver == nil {
+		return nil, fmt.Errorf("missing environment resolver")
+	}
 
 	syncSessionPool := syncSessionPool{
 		syncSessions: make(map[string]syncSession),
 	}
 
-	trigger := controltowerv1.ToolTrigger_TOOL_TRIGGER_MANUAL
-	source := packagev1.ProjectSourceType_PROJECT_SOURCE_TYPE_UNSPECIFIED
+	done := make(chan bool)
+	self := &syncReporter{
+		config:      &config,
+		done:        done,
+		workQueue:   make(chan *workItem, 1000),
+		client:      config.ClientConnection,
+		sessions:    &syncSessionPool,
+		callbacks:   callbacks,
+		envResolver: envResolver,
+	}
 
 	// A multi-project sync is required for cases like GitHub org where
 	// we are scanning multiple repositories
@@ -159,14 +212,7 @@ func NewSyncReporter(config SyncReporterConfig, callbacks SyncReporterCallbacks)
 		// Refactor this into a common session creator function
 		toolServiceClient := controltowerv1grpc.NewToolServiceClient(config.ClientConnection)
 		toolSessionRes, err := toolServiceClient.CreateToolSession(context.Background(),
-			&controltowerv1.CreateToolSessionRequest{
-				ToolName:       config.Tool.Name,
-				ToolVersion:    config.Tool.Version,
-				ProjectName:    config.ProjectName,
-				ProjectVersion: &config.ProjectVersion,
-				ProjectSource:  &source,
-				Trigger:        &trigger,
-			})
+			self.createToolSessionRequestForProjectVersion(config.ProjectName, config.ProjectVersion))
 		if err != nil {
 			return nil, fmt.Errorf("failed to create tool session: %w", err)
 		}
@@ -178,16 +224,6 @@ func NewSyncReporter(config SyncReporterConfig, callbacks SyncReporterCallbacks)
 			toolServiceClient)
 	}
 
-	done := make(chan bool)
-	self := &syncReporter{
-		config:    &config,
-		done:      done,
-		workQueue: make(chan *workItem, 1000),
-		client:    config.ClientConnection,
-		sessions:  &syncSessionPool,
-		callbacks: callbacks,
-	}
-
 	self.dispatchOnSyncStart()
 	self.startWorkers()
 	return self, nil
@@ -203,23 +239,12 @@ func (s *syncReporter) AddManifest(manifest *models.PackageManifest) {
 		projectName := manifest.GetSource().GetNamespace()
 		projectVersion := "main"
 
-		source := packagev1.ProjectSourceType_PROJECT_SOURCE_TYPE_UNSPECIFIED
-		trigger := controltowerv1.ToolTrigger_TOOL_TRIGGER_MANUAL
-
 		logger.Debugf("Report Sync: Creating tool session for project: %s, version: %s",
 			projectName, projectVersion)
 
-		// Refactor this into a common session creator function
 		toolServiceClient := controltowerv1grpc.NewToolServiceClient(s.client)
 		toolSessionRes, err := toolServiceClient.CreateToolSession(context.Background(),
-			&controltowerv1.CreateToolSessionRequest{
-				ToolName:       s.config.Tool.Name,
-				ToolVersion:    s.config.Tool.Version,
-				ProjectName:    projectName,
-				ProjectVersion: &projectVersion,
-				ProjectSource:  &source,
-				Trigger:        &trigger,
-			})
+			s.createToolSessionRequestForProjectVersion(projectName, projectVersion))
 		if err != nil {
 			logger.Errorf("failed to create tool session for project: %s/%s: %v",
 				projectName, projectVersion, err)
@@ -547,3 +572,42 @@ func (s *syncReporter) syncPackage(pkg *models.Package) error {
 	s.dispatchOnPackageSyncDone(pkg)
 	return nil
 }
+
+func (s *syncReporter) createToolSessionRequestForProjectVersion(projectName, projectVersion string) *controltowerv1.CreateToolSessionRequest {
+	source := packagev1.ProjectSourceType_PROJECT_SOURCE_TYPE_UNSPECIFIED
+	trigger := s.envResolver.Trigger()
+	originSource := s.envResolver.GetProjectSource()
+	originUrl := s.envResolver.GetProjectUrl()
+	gitRef := s.envResolver.GitRef()
+	gitSha := s.envResolver.GitSha()
+
+	req := &controltowerv1.CreateToolSessionRequest{
+		ToolName:       s.config.Tool.Name,
+		ToolVersion:    s.config.Tool.Version,
+		ProjectName:    projectName,
+		ProjectVersion: &projectVersion,
+		ProjectSource:  &source,
+	}
+
+	if trigger != controltowerv1.ToolTrigger_TOOL_TRIGGER_UNSPECIFIED {
+		req.Trigger = &trigger
+	}
+
+	if originSource != controltowerv1pb.Project_SOURCE_UNSPECIFIED {
+		req.OriginProjectSource = &originSource
+	}
+
+	if originUrl != "" {
+		req.OriginProjectUrl = &originUrl
+	}
+
+	if gitRef != "" {
+		req.GitRef = &gitRef
+	}
+
+	if gitSha != "" {
+		req.GitSha = &gitSha
+	}
+
+	return req
+}
diff --git a/scan.go b/scan.go
index a90d820..0aafaac 100644
--- a/scan.go
+++ b/scan.go
@@ -647,7 +647,7 @@ func internalStartScan() error {
 			ProjectVersion:         syncReportStream,
 			EnableMultiProjectSync: syncEnableMultiProject,
 			ClientConnection:       clientConn,
-		}, reporter.SyncReporterCallbacks{
+		}, reporter.DefaultSyncReporterEnvResolver(), reporter.SyncReporterCallbacks{
 			OnSyncStart: func() {
 				ui.PrintMsg("🌐 Syncing data to SafeDep Cloud...")
 			},


Test patch:
diff --git a/pkg/reporter/sync_test.go b/pkg/reporter/sync_test.go
index a3b2b3b..4a3f489 100644
--- a/pkg/reporter/sync_test.go
+++ b/pkg/reporter/sync_test.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"testing"
 
+	controltowerv1pb "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/controltower/v1"
 	malysisv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/malysis/v1"
 	packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
 	vulnerabilityv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/vulnerability/v1"
@@ -571,3 +572,107 @@ func TestSyncEvent(t *testing.T) {
 		})
 	}
 }
+
+type testEnvResolver struct {
+	projectSource controltowerv1pb.Project_Source
+	projectUrl    string
+	trigger       controltowerv1.ToolTrigger
+	gitRef        string
+	gitSha        string
+}
+
+func (r *testEnvResolver) GetProjectSource() controltowerv1pb.Project_Source {
+	return r.projectSource
+}
+
+func (r *testEnvResolver) GetProjectUrl() string {
+	return r.projectUrl
+}
+
+func (r *testEnvResolver) Trigger() controltowerv1.ToolTrigger {
+	return r.trigger
+}
+
+func (r *testEnvResolver) GitRef() string {
+	return r.gitRef
+}
+
+func (r *testEnvResolver) GitSha() string {
+	return r.gitSha
+}
+
+func TestCreateToolSessionRequestForProjectVersion(t *testing.T) {
+	cases := []struct {
+		name        string
+		envResolver SyncReporterEnvResolver
+		config      *SyncReporterConfig
+		assertFn    func(t *testing.T, request *controltowerv1.CreateToolSessionRequest)
+	}{
+		{
+			name: "default",
+			config: &SyncReporterConfig{
+				Tool: ToolMetadata{
+					Name:    "test-tool",
+					Version: "1.0.0",
+				},
+				ProjectName:    "test-project",
+				ProjectVersion: "1.0.0",
+			},
+			envResolver: DefaultSyncReporterEnvResolver(),
+			assertFn: func(t *testing.T, request *controltowerv1.CreateToolSessionRequest) {
+				assert.Equal(t, "test-tool", request.ToolName)
+				assert.Equal(t, "1.0.0", request.ToolVersion)
+				assert.Equal(t, "test-project", request.ProjectName)
+				assert.Equal(t, "1.0.0", *request.ProjectVersion)
+				assert.Equal(t, packagev1.ProjectSourceType_PROJECT_SOURCE_TYPE_UNSPECIFIED, *request.ProjectSource)
+				assert.Equal(t, controltowerv1.ToolTrigger_TOOL_TRIGGER_MANUAL, *request.Trigger)
+				assert.Nil(t, request.OriginProjectSource)
+				assert.Nil(t, request.OriginProjectUrl)
+				assert.Nil(t, request.GitRef)
+				assert.Nil(t, request.GitSha)
+			},
+		},
+		{
+			name: "with resolved attributes",
+			config: &SyncReporterConfig{
+				Tool: ToolMetadata{
+					Name:    "test-tool",
+					Version: "1.0.0",
+				},
+				ProjectName:    "test-project",
+				ProjectVersion: "1.0.0",
+			},
+			envResolver: &testEnvResolver{
+				projectSource: controltowerv1pb.Project_SOURCE_GITHUB,
+				projectUrl:    "https://github.com/test/test",
+				trigger:       controltowerv1.ToolTrigger_TOOL_TRIGGER_MANUAL,
+				gitRef:        "refs/heads/main",
+				gitSha:        "1234567890",
+			},
+			assertFn: func(t *testing.T, request *controltowerv1.CreateToolSessionRequest) {
+				assert.Equal(t, "test-tool", request.ToolName)
+				assert.Equal(t, "1.0.0", request.ToolVersion)
+				assert.Equal(t, "test-project", request.ProjectName)
+				assert.Equal(t, "1.0.0", *request.ProjectVersion)
+				assert.Equal(t, packagev1.ProjectSourceType_PROJECT_SOURCE_TYPE_UNSPECIFIED, *request.ProjectSource)
+				assert.Equal(t, controltowerv1.ToolTrigger_TOOL_TRIGGER_MANUAL, *request.Trigger)
+				assert.Equal(t, controltowerv1pb.Project_SOURCE_GITHUB, *request.OriginProjectSource)
+				assert.Equal(t, "https://github.com/test/test", *request.OriginProjectUrl)
+				assert.Equal(t, "refs/heads/main", *request.GitRef)
+				assert.Equal(t, "1234567890", *request.GitSha)
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			sr := &syncReporter{
+				config:      tc.config,
+				envResolver: tc.envResolver,
+			}
+
+			request := sr.createToolSessionRequestForProjectVersion(tc.config.ProjectName, tc.config.ProjectVersion)
+			tc.assertFn(t, request)
+		})
+	}
+}