Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
)

// replace github.com/jfrog/jfrog-cli-security => github.com/jfog/jfrog-cli-security dev
replace github.com/jfrog/jfrog-cli-security => github.com/eranturgeman/jfrog-cli-security v0.0.0-20260816144519-2c7540ad28a0

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

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/eranturgeman/jfrog-cli-security v0.0.0-20260816144519-2c7540ad28a0 h1:2dwG+Kzqvx8ycxAKI7fYCasjM68GeoKfq/o/oto1bS4=
github.com/eranturgeman/jfrog-cli-security v0.0.0-20260816144519-2c7540ad28a0/go.mod h1:J+GxgmNuyCZxb0BJnXbPQq1NEbtyWKMJR9kGwN2VJHE=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
Expand Down Expand Up @@ -154,8 +156,6 @@ github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260812053557-d311dd344da9 h1:6
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260812053557-d311dd344da9/go.mod h1:eYyYY+GMdddy75/rCWiKg8EydGp/jRY+9+30QkGpbGw=
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260813123937-e7fa28b75506 h1:LgAoLyXkXzQ3AaJ10YVq1HAl38vVy75kFElDb6nEgEM=
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260813123937-e7fa28b75506/go.mod h1:gf7aUg/G9JyltCNhwMD5RVEsFzUCKPWKXRcTXSqMYBk=
github.com/jfrog/jfrog-cli-security v1.34.0 h1:M4av+ff+EfSTZ7pK85Mm2fWlllNbHS0ndB4tZRRNXc8=
github.com/jfrog/jfrog-cli-security v1.34.0/go.mod h1:J+GxgmNuyCZxb0BJnXbPQq1NEbtyWKMJR9kGwN2VJHE=
github.com/jfrog/jfrog-client-go v1.55.1-0.20260813100550-0f2168d02558 h1:/4ayHXxzgyZ9f66EqImCyZr2SKUpygU1P36sDVAlskM=
github.com/jfrog/jfrog-client-go v1.55.1-0.20260813100550-0f2168d02558/go.mod h1:7B7eMRKuMhZ0rOdMItbJVpWjRUe1L//J3Jq+PgjiNxI=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
Expand Down
113 changes: 110 additions & 3 deletions scanpullrequest/scanpullrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@ import (
"context"
"errors"
"fmt"
"github.com/jfrog/gofrog/datastructures"
"os"
"path/filepath"
"regexp"
"slices"
"strings"

"github.com/jfrog/gofrog/datastructures"

"github.com/jfrog/froggit-go/vcsclient"
"github.com/jfrog/froggit-go/vcsutils"
"github.com/jfrog/jfrog-cli-security/utils/formats"
"github.com/jfrog/jfrog-cli-security/utils/jasutils"
"github.com/jfrog/jfrog-cli-security/utils/results"
"github.com/jfrog/jfrog-cli-security/utils/results/conversion"
"github.com/jfrog/jfrog-cli-security/utils/techutils"
"github.com/jfrog/jfrog-cli-security/utils/xsc"
"github.com/jfrog/jfrog-client-go/utils/log"

Expand All @@ -28,6 +32,15 @@ const (
analyticsScanPrScanType = "PR"
vulnerabilitiesFilteringErrorMessage = "%s scan has completed with errors. Vulnerabilities results will be removed from final report"
violationsFilteringErrorMessage = "%s scan has completed with errors. Violations results will be removed from final report"
noGitHubEnvErr = "frogbot did not scan this pull request, because a GitHub Environment named 'frogbot' does not exist. " +
"This repository requires it because it uses Maven and/or Gradle, whose dependency-tree resolution plugins execute build-tool code from the pull request before it can be scanned. " +
"Please refer to the Frogbot documentation for instructions on how to create the Environment"
noGitHubEnvReviewersErr = "frogbot did not scan this pull request, because the existing GitHub Environment named 'frogbot' doesn't have reviewers selected. " +
"This repository requires it because it uses Maven and/or Gradle, whose dependency-tree resolution plugins execute build-tool code from the pull request before it can be scanned. " +
"Please refer to the Frogbot documentation for instructions on how to create the Environment"
noGitHubEnvInWorkflowErr = "frogbot did not scan this pull request, because the workflow file does not set 'environment: frogbot'. " +
"This repository requires it because it uses Maven and/or Gradle, whose dependency-tree resolution plugins execute build-tool code from the pull request before it can be scanned. " +
"Please refer to the Frogbot documentation for instructions on how to configure the Environment"
)

// targetPair represents a matched pair of source and target scan results
Expand Down Expand Up @@ -102,12 +115,106 @@ func createBaseScanDetails(repoConfig *utils.Repository, client vcsclient.VcsCli
if err != nil {
return
}
return utils.NewScanDetails(client, &repoConfig.Server, &repoConfig.Params.Git).
scanDetails = utils.NewScanDetails(client, &repoConfig.Server, &repoConfig.Params.Git).
SetJfrogVersions(repoConfig.Params.XrayVersion, repoConfig.Params.XscVersion).
SetResultsContext(repositoryCloneUrl, repoConfig.Params.JFrogPlatform.JFrogProjectKey, false).
SetConfigProfile(repoConfig.Params.ConfigProfile).
SetDiffScan(true).
SetXscPRGitInfoContext(repoConfig.Params.Git.Project, client, repoConfig.Params.Git.PullRequestDetails), nil
SetXscPRGitInfoContext(repoConfig.Params.Git.Project, client, repoConfig.Params.Git.PullRequestDetails)
if repoConfig.Params.Git.GitProvider == vcsutils.GitHub {
scanDetails.SetRiskyTechEnvironmentGuard(blockRiskyTechsWithoutEnvironmentGuard(client, repoConfig))
}
return scanDetails, nil
}

func blockRiskyTechsWithoutEnvironmentGuard(client vcsclient.VcsClient, repoConfig *utils.Repository) func(detectedTechnologies []techutils.Technology) error {
return func(detectedTechnologies []techutils.Technology) error {
if !slices.Contains(detectedTechnologies, techutils.Maven) && !slices.Contains(detectedTechnologies, techutils.Gradle) {
return nil
}
return verifyGitHubFrogbotEnvironment(client, repoConfig)
}
}

// Verify that the 'frogbot' GitHub environment was properly configured on the repository
func verifyGitHubFrogbotEnvironment(client vcsclient.VcsClient, repoConfig *utils.Repository) error {
if repoConfig.Params.Git.APIEndpoint != "" && repoConfig.Params.Git.APIEndpoint != "https://api.github.com" {
// Don't verify 'frogbot' environment on GitHub on-prem
return nil
}
if _, exist := os.LookupEnv(utils.GitHubActionsEnv); !exist {
// Don't verify 'frogbot' environment on non GitHub Actions CI
return nil
}

// If the repository is not public, using 'frogbot' environment is not mandatory
repoInfo, err := client.GetRepositoryInfo(context.Background(), repoConfig.Params.Git.RepoOwner, repoConfig.Params.Git.RepoName)
if err != nil {
return err
}
if repoInfo.RepositoryVisibility != vcsclient.Public {
return nil
}

// Get the 'frogbot' environment info and make sure it exists and includes reviewers
repoEnvInfo, err := client.GetRepositoryEnvironmentInfo(context.Background(), repoConfig.Params.Git.RepoOwner, repoConfig.Params.Git.RepoName, "frogbot")
if err != nil {
return errors.New(err.Error() + "\n" + noGitHubEnvErr)
}
if len(repoEnvInfo.Reviewers) == 0 {
return errors.New(noGitHubEnvReviewersErr)
}

return verifyWorkflowContainsFrogbotEnvironment(client)
}

// Fetches the workflow file that triggered Frogbot (via GITHUB_WORKFLOW_REF) and verifies it contains 'environment: frogbot'.
func verifyWorkflowContainsFrogbotEnvironment(client vcsclient.VcsClient) error {
workflowRef := os.Getenv(utils.GitHubWorkflowRefEnv)
if workflowRef == "" {
return nil
}

// GITHUB_WORKFLOW_REF format: {owner}/{repo}/{path}@{ref}
// e.g. eranturgeman/jfrog-security-test-app/.github/workflows/frogbot-scan-pull-request.yml@refs/heads/main
// Note: the owner/repo here is the workflow's repo, which may differ from the scanned repo.
atIdx := strings.LastIndex(workflowRef, "@")
if atIdx == -1 {
return nil
}
pathPart := workflowRef[:atIdx]
ref := workflowRef[atIdx+1:]

// Parse owner, repo, and file path from "{owner}/{repo}/{path}"
parts := strings.SplitN(pathPart, "/", 3)
if len(parts) < 3 {
return nil
}
owner, repo, filePath := parts[0], parts[1], parts[2]

// Extract branch name from ref (e.g. "refs/heads/main" → "main")
branch := ref
if b, ok := strings.CutPrefix(ref, "refs/heads/"); ok {
branch = b
} else if b, ok := strings.CutPrefix(ref, "refs/tags/"); ok {
branch = b
}

fileContent, _, err := client.DownloadFileFromRepo(context.Background(), owner, repo, branch, filePath)
if err != nil {
// Can't fetch the file — skip this check rather than blocking the scan
log.Warn(fmt.Sprintf("Failed to fetch workflow file '%s' for environment verification: %s", filePath, err.Error()))
return nil
}
Comment thread
eranturgeman marked this conversation as resolved.

matched, err := regexp.MatchString(`\n\s*environment\s*:\s*frogbot`, string(fileContent))
Comment thread
eranturgeman marked this conversation as resolved.
Outdated
if err != nil {
return err
}
if !matched {
return errors.New(noGitHubEnvInWorkflowErr)
}
return nil
}

func downloadSourceAndTarget(repoConfig *utils.Repository, scanDetails *utils.ScanDetails) (sourceBranchWd, targetBranchWd string, cleanup func() error, err error) {
Expand Down
160 changes: 157 additions & 3 deletions scanpullrequest/scanpullrequest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import (
"context"
"errors"
"fmt"
"github.com/CycloneDX/cyclonedx-go"
"github.com/jfrog/jfrog-cli-security/utils/formats/violationutils"
services2 "github.com/jfrog/jfrog-client-go/xsc/services"
"net/http"
"net/http/httptest"
"os"
Expand All @@ -16,10 +13,15 @@ import (
"testing"
"time"

"github.com/CycloneDX/cyclonedx-go"
"github.com/jfrog/jfrog-cli-security/utils/formats/violationutils"
services2 "github.com/jfrog/jfrog-client-go/xsc/services"

"github.com/golang/mock/gomock"
securityutils "github.com/jfrog/jfrog-cli-security/utils"
"github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils"
"github.com/jfrog/jfrog-cli-security/utils/severityutils"
"github.com/jfrog/jfrog-cli-security/utils/techutils"
"github.com/jfrog/jfrog-cli-security/utils/xsc"
"github.com/jfrog/jfrog-client-go/xray/services"
"github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif"
Expand Down Expand Up @@ -69,6 +71,158 @@ func CreateMockVcsClient(t *testing.T) *testdata.MockVcsClient {
return testdata.NewMockVcsClient(gomock.NewController(t))
}

var gitParams = &utils.Repository{
OutputWriter: &outputwriter.SimplifiedOutput{},
Params: utils.Params{
Git: utils.Git{
RepoOwner: "repo-owner",
Branches: []string{"master"},
RepoName: "repo-name",
},
},
}

func TestRiskyTechEnvironmentGuard(t *testing.T) {
expectEnvironmentConfigured := func(client *testdata.MockVcsClient) {
client.EXPECT().GetRepositoryInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName).Return(vcsclient.RepositoryInfo{}, nil)
client.EXPECT().GetRepositoryEnvironmentInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName, "frogbot").Return(vcsclient.RepositoryEnvironmentInfo{Reviewers: []string{"froggy"}}, nil)
}

tests := []struct {
testName string
detected []techutils.Technology
setupMock func(client *testdata.MockVcsClient)
}{
{
testName: "no maven or gradle detected - no-op",
detected: []techutils.Technology{techutils.Npm, techutils.Pip},
},
{
testName: "no technologies detected - no-op",
detected: nil,
},
{
testName: "maven detected - delegates to environment verification",
detected: []techutils.Technology{techutils.Maven},
setupMock: expectEnvironmentConfigured,
},
{
testName: "gradle detected - delegates to environment verification",
detected: []techutils.Technology{techutils.Gradle},
setupMock: expectEnvironmentConfigured,
},
{
testName: "maven alongside other technologies - delegates to environment verification",
detected: []techutils.Technology{techutils.Npm, techutils.Maven},
setupMock: expectEnvironmentConfigured,
},
}
for _, test := range tests {
t.Run(test.testName, func(t *testing.T) {
// When setupMock is nil, no .EXPECT() is set on the mock: any VCS call fails the test,
// proving the guard is a pure no-op for non-Maven/Gradle technologies.
client := CreateMockVcsClient(t)
if test.setupMock != nil {
test.setupMock(client)
assert.NoError(t, os.Setenv(utils.GitHubActionsEnv, "true"))
assert.NoError(t, os.Unsetenv(utils.GitHubWorkflowRefEnv))
}

guard := blockRiskyTechsWithoutEnvironmentGuard(client, gitParams)
assert.NoError(t, guard(test.detected))
})
}
}

func TestVerifyGitHubFrogbotEnvironment(t *testing.T) {
tests := []struct {
testName string
repoConfig *utils.Repository
setupMock func(client *testdata.MockVcsClient)
wantErrContains string
}{
{
testName: "environment configured with reviewers",
repoConfig: gitParams,
setupMock: func(client *testdata.MockVcsClient) {
client.EXPECT().GetRepositoryInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName).Return(vcsclient.RepositoryInfo{}, nil)
client.EXPECT().GetRepositoryEnvironmentInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName, "frogbot").Return(vcsclient.RepositoryEnvironmentInfo{Reviewers: []string{"froggy"}}, nil)
},
},
{
testName: "environment does not exist",
repoConfig: gitParams,
setupMock: func(client *testdata.MockVcsClient) {
client.EXPECT().GetRepositoryInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName).Return(vcsclient.RepositoryInfo{}, nil)
client.EXPECT().GetRepositoryEnvironmentInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName, "frogbot").Return(vcsclient.RepositoryEnvironmentInfo{}, errors.New("404"))
},
wantErrContains: noGitHubEnvErr,
},
{
testName: "environment has no reviewers",
repoConfig: gitParams,
setupMock: func(client *testdata.MockVcsClient) {
client.EXPECT().GetRepositoryInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName).Return(vcsclient.RepositoryInfo{}, nil)
client.EXPECT().GetRepositoryEnvironmentInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName, "frogbot").Return(vcsclient.RepositoryEnvironmentInfo{}, nil)
},
wantErrContains: noGitHubEnvReviewersErr,
},
{
testName: "on-prem GitHub is skipped",
repoConfig: &utils.Repository{
Params: utils.Params{Git: utils.Git{VcsInfo: vcsclient.VcsInfo{APIEndpoint: "https://acme.vcs.io"}}},
},
},
}
for _, test := range tests {
t.Run(test.testName, func(t *testing.T) {
var client vcsclient.VcsClient
if test.setupMock != nil {
mockClient := CreateMockVcsClient(t)
test.setupMock(mockClient)
client = mockClient
} else {
client = &vcsclient.GitHubClient{}
}
assert.NoError(t, os.Setenv(utils.GitHubActionsEnv, "true"))
// Unset GITHUB_WORKFLOW_REF to avoid triggering the workflow file check
assert.NoError(t, os.Unsetenv(utils.GitHubWorkflowRefEnv))

err := verifyGitHubFrogbotEnvironment(client, test.repoConfig)
if test.wantErrContains != "" {
assert.ErrorContains(t, err, test.wantErrContains)
} else {
assert.NoError(t, err)
}
})
}
}

func TestVerifyWorkflowContainsFrogbotEnvironment(t *testing.T) {
workflowRef := "jfrog/frogbot/.github/workflows/frogbot.yml@refs/heads/main"
workflowContent := []byte("jobs:\n scan:\n environment: frogbot\n")

t.Run("environment field present", func(t *testing.T) {
client := CreateMockVcsClient(t)
client.EXPECT().DownloadFileFromRepo(context.Background(), "jfrog", "frogbot", "main", ".github/workflows/frogbot.yml").Return(workflowContent, 200, nil)
assert.NoError(t, os.Setenv(utils.GitHubWorkflowRefEnv, workflowRef))
assert.NoError(t, verifyWorkflowContainsFrogbotEnvironment(client))
})

t.Run("environment field missing", func(t *testing.T) {
client := CreateMockVcsClient(t)
client.EXPECT().DownloadFileFromRepo(context.Background(), "jfrog", "frogbot", "main", ".github/workflows/frogbot.yml").Return([]byte("jobs:\n scan:\n runs-on: ubuntu-latest\n"), 200, nil)
assert.NoError(t, os.Setenv(utils.GitHubWorkflowRefEnv, workflowRef))
assert.ErrorContains(t, verifyWorkflowContainsFrogbotEnvironment(client), noGitHubEnvInWorkflowErr)
})

t.Run("workflow ref not set", func(t *testing.T) {
client := CreateMockVcsClient(t)
assert.NoError(t, os.Unsetenv(utils.GitHubWorkflowRefEnv))
assert.NoError(t, verifyWorkflowContainsFrogbotEnvironment(client))
})
}

func TestScanResultsToIssuesCollection(t *testing.T) {
auditResults := &results.SecurityCommandResults{ResultsMetaData: results.ResultsMetaData{Entitlements: results.Entitlements{Jas: true}, ResultContext: results.ResultContext{IncludeVulnerabilities: true}}, Targets: []*results.TargetResults{{
ResultsStatus: results.ResultsStatus{
Expand Down
5 changes: 5 additions & 0 deletions utils/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ const (
GitlabScanResultsOutputDirEnv = "JF_SCAN_RESULTS_OUTPUT_DIR"
GitWorkspaceEnv = "JF_GIT_WORKSPACE"

// The 'GITHUB_ACTIONS' environment variable exists when the CI is GitHub Actions
GitHubActionsEnv = "GITHUB_ACTIONS"
// The 'GITHUB_WORKFLOW_REF' environment variable contains the ref path to the workflow file, e.g. owner/repo/.github/workflows/frogbot.yml@refs/heads/main
GitHubWorkflowRefEnv = "GITHUB_WORKFLOW_REF"

// Placeholders for templates
PackagePlaceHolder = "{IMPACTED_PACKAGE}"
FixVersionPlaceHolder = "{FIX_VERSION}"
Expand Down
Loading
Loading