Skip to content

Commit 69e8ec5

Browse files
authored
Merge branch 'dev' into patch-3
2 parents d3dd538 + addbb05 commit 69e8ec5

8 files changed

Lines changed: 350 additions & 30 deletions

File tree

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/jfrog/frogbot/v2
33
go 1.24.2
44

55
require (
6+
github.com/CycloneDX/cyclonedx-go v0.9.2
67
github.com/go-git/go-git/v5 v5.16.0
78
github.com/golang/mock v1.6.0
89
github.com/google/go-github/v45 v45.2.0
@@ -25,7 +26,6 @@ require (
2526
require (
2627
dario.cat/mergo v1.0.1 // indirect
2728
github.com/BurntSushi/toml v1.4.0 // indirect
28-
github.com/CycloneDX/cyclonedx-go v0.9.2 // indirect
2929
github.com/Microsoft/go-winio v0.6.2 // indirect
3030
github.com/ProtonMail/go-crypto v1.1.6 // indirect
3131
github.com/andybalholm/brotli v1.1.1 // indirect
@@ -125,7 +125,7 @@ require (
125125
gopkg.in/warnings.v0 v0.1.2 // indirect
126126
)
127127

128-
// replace github.com/jfrog/jfrog-cli-security => github.com/jfrog/jfrog-cli-security dev
128+
replace github.com/jfrog/jfrog-cli-security => github.com/jfrog/jfrog-cli-security v1.20.3-0.20250803110644-e1c158a10a1e
129129

130130
// replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 dev
131131

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ github.com/jfrog/jfrog-cli-artifactory v0.5.1 h1:C1H8x6ldg7VmKIb4F8OZKm0PJbBrlA7
139139
github.com/jfrog/jfrog-cli-artifactory v0.5.1/go.mod h1:RXz0AZ5qVEOhanXRGmC4+atqr29ZtIeV5kUCbNGtVj4=
140140
github.com/jfrog/jfrog-cli-core/v2 v2.59.3 h1:AlFC9l3Cwvx2FzuAP0txxEjZPDjv5bpO7YPTOjHp7HA=
141141
github.com/jfrog/jfrog-cli-core/v2 v2.59.3/go.mod h1:Itfva9wqPMkUVBuNpKw0Oe4qoPlt0PdXfmWqf7fBebU=
142-
github.com/jfrog/jfrog-cli-security v1.20.2 h1:GGpSdEnxcmUbK5Iinm8LN0EUG9KAm1+K958yYtcqxeQ=
143-
github.com/jfrog/jfrog-cli-security v1.20.2/go.mod h1:3sDIo2prTKwB2IeiWk7Hqxxags8obqRi5uNnh9i9XIc=
142+
github.com/jfrog/jfrog-cli-security v1.20.3-0.20250803110644-e1c158a10a1e h1:37Wxxr9qbt2dSuvYCcMN3YNsTWVVhfnSBf3SI+l5vas=
143+
github.com/jfrog/jfrog-cli-security v1.20.3-0.20250803110644-e1c158a10a1e/go.mod h1:3sDIo2prTKwB2IeiWk7Hqxxags8obqRi5uNnh9i9XIc=
144144
github.com/jfrog/jfrog-client-go v1.54.3 h1:tIhhDhM7rDMT1lzgeQy4nr6H2ihlqnumhiFybIWlLz0=
145145
github.com/jfrog/jfrog-client-go v1.54.3/go.mod h1:1v0eih4thdPA4clBo9TuvAMT25sGDr1IQJ81DXQ/lBY=
146146
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=

scanrepository/scanrepository.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ func (cfp *ScanRepositoryCmd) scanAndFixProject(repository *utils.Repository) (i
191191
if err = utils.UploadSarifResultsToGithubSecurityTab(scanResults, repository, cfp.scanDetails.BaseBranch(), cfp.scanDetails.Client()); err != nil {
192192
log.Warn(err)
193193
}
194+
195+
if *repository.UploadSbomToVcs && scanResults.EntitledForJas {
196+
if err = utils.UploadSbomSnapshotToGithubDependencyGraph(repository.RepoOwner, repository.RepoName, scanResults, cfp.scanDetails.Client(), cfp.scanDetails.BaseBranch()); err != nil {
197+
log.Warn(err)
198+
}
199+
}
194200
}
195201
if repository.DetectionOnly {
196202
continue

utils/consts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const (
4242
GitUsernameEnv = "JF_GIT_USERNAME"
4343
GitUseLocalRepositoryEnv = "JF_USE_LOCAL_REPOSITORY"
4444
UseMostCommonAncestorAsTargetEnv = "JF_USE_MOST_COMMON_ANCESTOR_AS_TARGET"
45+
GitDependencyGraphSubmissionEnv = "JF_UPLOAD_SBOM_TO_VCS"
4546

4647
// Git naming template environment variables
4748
BranchNameTemplateEnv = "JF_BRANCH_NAME_TEMPLATE"

utils/params.go

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ type FrogbotDetails struct {
5151

5252
type RepoAggregator []Repository
5353

54-
// NewRepoAggregator returns an initialized RepoAggregator with an empty repository
54+
// Returns an initialized RepoAggregator with an empty repository
5555
func newRepoAggregator() RepoAggregator {
5656
return RepoAggregator{{Params: Params{Scan: Scan{Projects: []Project{{}}}}}}
5757
}
@@ -336,6 +336,7 @@ type Git struct {
336336
PullRequestDetails vcsclient.PullRequestInfo
337337
RepositoryCloneUrl string
338338
UseLocalRepository bool
339+
UploadSbomToVcs *bool `yaml:"uploadSbomToVcs,omitempty"`
339340
}
340341

341342
func (g *Git) GetRepositoryHttpsCloneUrl(gitClient vcsclient.VcsClient) (string, error) {
@@ -377,6 +378,17 @@ func (g *Git) setDefaultsIfNeeded(gitParamsFromEnv *Git, commandName string) (er
377378
return
378379
}
379380
}
381+
382+
// We don't need to examine gitParamsFromEnv since GitDependencyGraphSubmissionEnv value is not fetched upon gitParamsFromEnv creation
383+
if g.UploadSbomToVcs == nil || !*g.UploadSbomToVcs {
384+
if g.UploadSbomToVcs == nil {
385+
envValue, err := getBoolEnv(GitDependencyGraphSubmissionEnv, true)
386+
if err != nil {
387+
return err
388+
}
389+
g.UploadSbomToVcs = &envValue
390+
}
391+
}
380392
return
381393
}
382394

@@ -394,15 +406,13 @@ func (g *Git) extractScanPullRequestEnvParams(gitParamsFromEnv *Git) (err error)
394406
}
395407
}
396408
if g.UseMostCommonAncestorAsTarget == nil || !*g.UseMostCommonAncestorAsTarget {
397-
defaultValue := true
398-
if g.UseMostCommonAncestorAsTarget != nil {
399-
defaultValue = *g.UseMostCommonAncestorAsTarget
400-
}
401-
envValue, err := getBoolEnv(UseMostCommonAncestorAsTargetEnv, defaultValue)
402-
if err != nil {
403-
return err
409+
if g.UseMostCommonAncestorAsTarget == nil {
410+
envValue, err := getBoolEnv(UseMostCommonAncestorAsTargetEnv, true)
411+
if err != nil {
412+
return err
413+
}
414+
g.UseMostCommonAncestorAsTarget = &envValue
404415
}
405-
g.UseMostCommonAncestorAsTarget = &envValue
406416
}
407417
g.AvoidExtraMessages, err = getBoolEnv(AvoidExtraMessages, false)
408418
return
@@ -508,7 +518,7 @@ func GetFrogbotDetails(commandName string) (frogbotDetails *FrogbotDetails, err
508518
return
509519
}
510520

511-
// getConfigAggregator returns a RepoAggregator based on frogbot-config.yml and environment variables.
521+
// Returns a RepoAggregator based on frogbot-config.yml and environment variables.
512522
func getConfigAggregator(xrayVersion, xscVersion string, gitClient vcsclient.VcsClient, gitParamsFromEnv *Git, jfrogServer *coreconfig.ServerDetails, commandName string) (RepoAggregator, error) {
513523
configFileContent, err := getConfigFileContent(gitClient, gitParamsFromEnv, commandName)
514524
if err != nil {
@@ -520,7 +530,7 @@ func getConfigAggregator(xrayVersion, xscVersion string, gitClient vcsclient.Vcs
520530
return BuildRepoAggregator(xrayVersion, xscVersion, gitClient, configFileContent, gitParamsFromEnv, jfrogServer, commandName)
521531
}
522532

523-
// getConfigFileContent retrieves the content of the frogbot-config.yml file
533+
// Retrieves the content of the frogbot-config.yml file
524534
func getConfigFileContent(gitClient vcsclient.VcsClient, gitParamsFromEnv *Git, commandName string) ([]byte, error) {
525535
var errMissingConfig *ErrMissingConfig
526536

@@ -543,7 +553,7 @@ func getConfigFileContent(gitClient vcsclient.VcsClient, gitParamsFromEnv *Git,
543553
return configFileContent, err
544554
}
545555

546-
// BuildRepoAggregator receives the content of a frogbot-config.yml file, along with the Git (built from environment variables) and ServerDetails parameters.
556+
// Receives the content of a frogbot-config.yml file, along with the Git (built from environment variables) and ServerDetails parameters.
547557
// Returns a RepoAggregator instance with all the defaults and necessary fields.
548558
func BuildRepoAggregator(xrayVersion, xscVersion string, gitClient vcsclient.VcsClient, configFileContent []byte, gitParamsFromEnv *Git, server *coreconfig.ServerDetails, commandName string) (resultAggregator RepoAggregator, err error) {
549559
var cleanAggregator RepoAggregator
@@ -566,7 +576,7 @@ func BuildRepoAggregator(xrayVersion, xscVersion string, gitClient vcsclient.Vcs
566576
return
567577
}
568578

569-
// unmarshalFrogbotConfigYaml uses the yaml.Unmarshaler interface to parse the yamlContent.
579+
// Uses the yaml.Unmarshaler interface to parse the yamlContent.
570580
// If there is no config file, the function returns a RepoAggregator with an empty repository.
571581
func unmarshalFrogbotConfigYaml(yamlContent []byte) (result RepoAggregator, err error) {
572582
if len(yamlContent) == 0 {
@@ -739,7 +749,7 @@ func SanitizeEnv() error {
739749
return nil
740750
}
741751

742-
// ReadConfigFromFileSystem looks for .frogbot/frogbot-config.yml from the given path and return its content. The path is relative and starts from the root of the project.
752+
// Looks for .frogbot/frogbot-config.yml from the given path and return its content. The path is relative and starts from the root of the project.
743753
// If the config file is not found in the relative path, it will search in parent dirs.
744754
func ReadConfigFromFileSystem(configRelativePath string) (configFileContent []byte, err error) {
745755
log.Debug("Reading config from file system. Looking for", osFrogbotConfigPath)

utils/testsutils.go

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package utils
33
import (
44
"encoding/json"
55
"fmt"
6-
"github.com/jfrog/jfrog-client-go/xsc/services"
6+
"io"
77
"net/http"
88
"net/http/httptest"
99
"os"
@@ -12,6 +12,8 @@ import (
1212
"testing"
1313
"time"
1414

15+
"github.com/jfrog/jfrog-client-go/xsc/services"
16+
1517
"github.com/go-git/go-git/v5"
1618
goGitConfig "github.com/go-git/go-git/v5/config"
1719
"github.com/go-git/go-git/v5/plumbing/object"
@@ -230,3 +232,98 @@ func CreateXscMockServerForConfigProfile(t *testing.T, xrayVersion string) (mock
230232
}
231233
return
232234
}
235+
236+
// CreateMockServerForDependencySubmission creates a mock GitHub server for testing dependency submission
237+
func CreateMockServerForDependencySubmission(t *testing.T, owner, repo string) *httptest.Server {
238+
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
239+
// Verify correct URL construction
240+
expectedPath := fmt.Sprintf("/repos/%s/%s/dependency-graph/snapshots", owner, repo)
241+
if r.URL.Path != expectedPath {
242+
t.Errorf("Expected path is '%s', got '%s'", expectedPath, r.URL.Path)
243+
w.WriteHeader(http.StatusNotFound)
244+
return
245+
}
246+
// Verify correct method
247+
if r.Method != http.MethodPost {
248+
t.Errorf("Expected POST method, got %s", r.Method)
249+
w.WriteHeader(http.StatusMethodNotAllowed)
250+
return
251+
}
252+
253+
// Read request body and parse it to ensure all mandatory fields exist
254+
body, err := io.ReadAll(r.Body)
255+
if err != nil {
256+
t.Errorf("Failed to read request body: %v", err)
257+
w.WriteHeader(http.StatusBadRequest)
258+
return
259+
}
260+
var snapshot map[string]interface{}
261+
if err := json.Unmarshal(body, &snapshot); err != nil {
262+
t.Errorf("Failed to parse request body as JSON: %v", err)
263+
w.WriteHeader(http.StatusBadRequest)
264+
return
265+
}
266+
267+
// Verify required fields are present
268+
requiredFields := []string{"version", "sha", "ref", "scanned", "job", "detector"}
269+
for _, field := range requiredFields {
270+
if _, exists := snapshot[field]; !exists {
271+
t.Errorf("Missing required field: %s", field)
272+
w.WriteHeader(http.StatusBadRequest)
273+
return
274+
}
275+
}
276+
277+
// Verify required job fields
278+
if job, ok := snapshot["job"].(map[string]interface{}); ok {
279+
if _, exists := job["id"]; !exists {
280+
t.Errorf("Missing job.id field")
281+
w.WriteHeader(http.StatusBadRequest)
282+
return
283+
}
284+
if _, exists := job["correlator"]; !exists {
285+
t.Errorf("Missing job.correlator field")
286+
w.WriteHeader(http.StatusBadRequest)
287+
return
288+
}
289+
}
290+
291+
// Verify required detector fields
292+
if detector, ok := snapshot["detector"].(map[string]interface{}); ok {
293+
if _, exists := detector["name"]; !exists {
294+
t.Errorf("Missing detector.name field")
295+
w.WriteHeader(http.StatusBadRequest)
296+
return
297+
}
298+
if _, exists := detector["version"]; !exists {
299+
t.Errorf("Missing detector.version field")
300+
w.WriteHeader(http.StatusBadRequest)
301+
return
302+
}
303+
if _, exists := detector["url"]; !exists {
304+
t.Errorf("Missing detector.url field")
305+
w.WriteHeader(http.StatusBadRequest)
306+
return
307+
}
308+
}
309+
310+
// Return success response
311+
w.WriteHeader(http.StatusCreated)
312+
}))
313+
}
314+
315+
// CreateMockServerForDependencySubmissionError creates a mock GitHub server that returns an error
316+
func CreateMockServerForDependencySubmissionError(t *testing.T, owner, repo string) *httptest.Server {
317+
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
318+
// Verify correct URL construction
319+
expectedPath := fmt.Sprintf("/repos/%s/%s/dependency-graph/snapshots", owner, repo)
320+
if r.URL.Path != expectedPath {
321+
t.Errorf("Expected path is '%s', got '%s'", expectedPath, r.URL.Path)
322+
w.WriteHeader(http.StatusNotFound)
323+
return
324+
}
325+
326+
// Return error response
327+
w.WriteHeader(http.StatusBadRequest)
328+
}))
329+
}

utils/utils.go

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/jfrog/jfrog-cli-core/v2/utils/usage"
2222
"github.com/jfrog/jfrog-cli-security/utils"
2323
"github.com/jfrog/jfrog-cli-security/utils/formats"
24+
"github.com/jfrog/jfrog-cli-security/utils/formats/snapshotconvertor"
2425
"github.com/jfrog/jfrog-cli-security/utils/results"
2526
"github.com/jfrog/jfrog-cli-security/utils/results/conversion"
2627
"github.com/jfrog/jfrog-cli-security/utils/results/output"
@@ -32,12 +33,14 @@ import (
3233
)
3334

3435
const (
35-
ScanPullRequest = "scan-pull-request"
36-
ScanAllPullRequests = "scan-all-pull-requests"
37-
ScanRepository = "scan-repository"
38-
ScanMultipleRepositories = "scan-multiple-repositories"
39-
RootDir = "."
40-
branchNameRegex = `[~^:?\\\[\]@{}*]`
36+
ScanPullRequest = "scan-pull-request"
37+
ScanAllPullRequests = "scan-all-pull-requests"
38+
ScanRepository = "scan-repository"
39+
ScanMultipleRepositories = "scan-multiple-repositories"
40+
RootDir = "."
41+
branchNameRegex = `[~^:?\\\[\]@{}*]`
42+
dependencySubmissionFrogbotDetector = "JFrog Frogbot"
43+
frogbotUrl = "https://github.com/jfrog/frogbot"
4144

4245
// Branch validation error messages
4346
branchInvalidChars = "branch name cannot contain the following chars ~, ^, :, ?, *, [, ], @, {, }"
@@ -236,6 +239,43 @@ func UploadSarifResultsToGithubSecurityTab(scanResults *results.SecurityCommandR
236239
return nil
237240
}
238241

242+
func UploadSbomSnapshotToGithubDependencyGraph(owner, repo string, scanResults *results.SecurityCommandResults, client vcsclient.VcsClient, branch string) error {
243+
if scanResults == nil {
244+
return fmt.Errorf("got an empty scan results")
245+
}
246+
247+
cyclonedxWithSbom, err := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{HasViolationContext: scanResults.HasViolationContext(), IncludeVulnerabilities: scanResults.IncludesVulnerabilities(), IncludeSbom: true}).ConvertToCycloneDx(scanResults)
248+
if err != nil {
249+
return fmt.Errorf("failed to convert results to CycloneDX format: %w", err)
250+
}
251+
var jobId, jobCorrelator, commitSha string
252+
if jobId = getTrimmedEnv(utils.CurrentGithubWorkflowJobEnvVar); jobId == "" {
253+
return fmt.Errorf("%s env var is empty and required for Github Dependency submission", utils.CurrentGithubWorkflowJobEnvVar)
254+
}
255+
workflowName := getTrimmedEnv(utils.CurrentGithubWorkflowNameEnvVar)
256+
if workflowName == "" {
257+
return fmt.Errorf("%s env var is empty and required for Github Dependency submission", utils.CurrentGithubWorkflowNameEnvVar)
258+
}
259+
jobCorrelator = fmt.Sprintf("%s_%s", workflowName, jobId)
260+
if commitSha = getTrimmedEnv(utils.CurrentGithubShaEnvVar); commitSha == "" {
261+
return fmt.Errorf("%s env var is empty and required for Github Dependency submission", utils.CurrentGithubShaEnvVar)
262+
}
263+
264+
snapshot, err := snapshotconvertor.CreateGithubSnapshotFromSbom(cyclonedxWithSbom, 0, scanResults.StartTime, jobId, jobCorrelator, commitSha, branch, dependencySubmissionFrogbotDetector, FrogbotVersion, frogbotUrl)
265+
if err != nil {
266+
return fmt.Errorf("failed to convert CycloneDX to SBOM snapshot: %w", err)
267+
}
268+
269+
if err = client.UploadSnapshotToDependencyGraph(context.Background(), owner, repo, snapshot); err != nil {
270+
snapshotJson, e := utils.GetAsJsonString(snapshot, false, true)
271+
if e != nil {
272+
return fmt.Errorf("failed to upload SBOM snapshot to GitHub: %w", err)
273+
}
274+
return fmt.Errorf("failed to upload SBOM snapshot to GitHub: %w\nSent Snapshot:\n%s", err, snapshotJson)
275+
}
276+
return nil
277+
}
278+
239279
func GenerateFrogbotSarifReport(extendedResults *results.SecurityCommandResults, allowedLicenses []string) (string, error) {
240280
convertor := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{
241281
IncludeVulnerabilities: extendedResults.IncludesVulnerabilities(),

0 commit comments

Comments
 (0)