Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions commands/curation/curationaudit.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import (
cargotech "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/cargo"
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/docker"
gemtech "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/gem"
_go "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/go"
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/huggingface"
hfdiscovery "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/huggingface/discovery"
npmtech "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/npm"
Expand Down Expand Up @@ -3298,11 +3299,17 @@ func getNugetNameScopeAndVersion(id, artiUrl, repo string) (downloadUrls []strin
// output: downloadUrl: <artiUrl>/api/go/go/github.com/kennygrant/sanitize/@v/v1.2.4.zip
func getGoNameScopeAndVersion(id, artiUrl, repo string) (downloadUrls []string, name, scope, version string) {
id = strings.TrimPrefix(id, techutils.Go.String()+"://")
// A module satisfied by a filesystem 'replace' directive was never published - skip probing it.
isLocalReplace := strings.HasSuffix(id, _go.LocalReplaceMarker)
id = strings.TrimSuffix(id, _go.LocalReplaceMarker)
nameVersion := strings.Split(id, ":")
name = nameVersion[0]
if len(nameVersion) > 1 {
version = nameVersion[1]
}
if isLocalReplace {
return nil, name, "", version
}
url := strings.TrimSuffix(artiUrl, "/") + "/api/go/" + repo + "/" + name + "/@v/" + version + ".zip"
return []string{url}, name, "", version
}
Expand Down
50 changes: 40 additions & 10 deletions commands/curation/curationaudit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"testing"

"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies"
_go "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/go"
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/java"
"github.com/jfrog/jfrog-cli-security/utils/formats"

Expand Down Expand Up @@ -685,21 +686,41 @@ func getTestCasesForDoCurationAudit() []testCase {
pathToProject: filepath.Join("projects", "package-managers", "go", "curation-project"),
createServerWithoutCreds: true,
serveResources: map[string]string{
"v1.5.2.mod": filepath.Join("resources", "quote-v1.5.2.mod"),
"v1.5.2.zip": filepath.Join("resources", "quote-v1.5.2.zip"),
"v1.5.2.info": filepath.Join("resources", "quote-v1.5.2.info"),
"v1.3.0.mod": filepath.Join("resources", "sampler-v1.3.0.mod"),
"v1.3.0.zip": filepath.Join("resources", "sampler-v1.3.0.zip"),
"v1.3.0.info": filepath.Join("resources", "sampler-v1.3.0.info"),
"v0.0.0-20170915032832-14c0d48ead0c.mod": filepath.Join("resources", "text-v0.0.0-20170915032832-14c0d48ead0c.mod"),
"v0.0.0-20170915032832-14c0d48ead0c.zip": filepath.Join("resources", "text-v0.0.0-20170915032832-14c0d48ead0c.zip"),
"v1.5.2.mod": filepath.Join("resources", "quote-v1.5.2.mod"),
"v1.5.2.zip": filepath.Join("resources", "quote-v1.5.2.zip"),
"v1.5.2.info": filepath.Join("resources", "quote-v1.5.2.info"),
"v1.3.0.mod": filepath.Join("resources", "sampler-v1.3.0.mod"),
"v1.3.0.zip": filepath.Join("resources", "sampler-v1.3.0.zip"),
"v1.3.0.info": filepath.Join("resources", "sampler-v1.3.0.info"),
"v0.0.0-20170915032832-14c0d48ead0c.mod": filepath.Join("resources", "text-v0.0.0-20170915032832-14c0d48ead0c.mod"),
"v0.0.0-20170915032832-14c0d48ead0c.zip": filepath.Join("resources", "text-v0.0.0-20170915032832-14c0d48ead0c.zip"),
"v0.0.0-20170915032832-14c0d48ead0c.info": filepath.Join("resources", "text-v0.0.0-20170915032832-14c0d48ead0c.info"),
},
// example.com/localmod is a tripwire, not an expected call: it's local-replaced and must never
// be probed. If a regression ever probes it, this mock 403s it, breaking expectedResp below.
requestToFail: map[string]bool{
"/api/go/go-virtual/rsc.io/sampler/@v/v1.3.0.zip": false,
"/api/go/go-virtual/rsc.io/sampler/@v/v1.3.0.zip": false,
"/api/go/go-virtual/example.com/localmod/@v/v0.0.0.zip": false,
Comment thread
Phavya-jfrog marked this conversation as resolved.
},
expectedResp: map[string]*CurationReport{
"github.com/you/hello": {packagesStatus: []*PackageStatus{
{
Action: "blocked",
ParentName: "example.com/localmod",
ParentVersion: "v0.0.0",
BlockedPackageUrl: "/api/go/go-virtual/rsc.io/sampler/@v/v1.3.0.zip",
PackageName: "rsc.io/sampler",
PackageVersion: "v1.3.0",
BlockingReason: "Policy violations",
DepRelation: "indirect",
PkgType: "go",
Policy: []Policy{
{
Policy: "pol1",
Condition: "cond1",
},
},
},
{
Action: "blocked",
ParentName: "rsc.io/quote",
Expand Down Expand Up @@ -735,7 +756,7 @@ func getTestCasesForDoCurationAudit() []testCase {
},
},
},
totalNumberOfPackages: 3,
totalNumberOfPackages: 4,
},
},
},
Expand Down Expand Up @@ -1230,6 +1251,15 @@ func Test_getGoNameScopeAndVersion(t *testing.T) {
compName: "github.com/kennygrant/sanitize",
version: "v1.2.4",
},
{
name: "local replace go component id is skipped",
compId: "go://github.com/example/localmod:v0.0.0" + _go.LocalReplaceMarker,
rtUrl: "http://test/artifactory",
repo: "test",
downloadUrls: nil,
compName: "github.com/example/localmod",
version: "v0.0.0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ require (
github.com/urfave/cli v1.22.17
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3
golang.org/x/mod v0.37.0
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/text v0.40.0
Expand Down Expand Up @@ -147,7 +148,6 @@ require (
go.opentelemetry.io/otel/trace v1.42.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/term v0.45.0 // indirect
Expand Down
68 changes: 63 additions & 5 deletions sca/bom/buildinfo/technologies/go/golang.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package _go

import (
"errors"
"os"
"path/filepath"
"strings"

biutils "github.com/jfrog/build-info-go/utils"
Expand All @@ -14,11 +16,15 @@ import (
"github.com/jfrog/jfrog-client-go/utils/errorutils"
"github.com/jfrog/jfrog-client-go/utils/log"
xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils"
"golang.org/x/mod/modfile"
)

const (
goPackageTypeIdentifier = "go://"
goSourceCodePrefix = "github.com/golang/go:v"
// LocalReplaceMarker tags a tree node whose module is satisfied by a local 'replace' directive.
// It's never published, so never probe it.
LocalReplaceMarker = ":local-replace"
)

func BuildDependencyTree(params technologies.BuildInfoBomGeneratorParams) (dependencyTree []*xrayUtils.GraphNode, uniqueDeps []string, err error) {
Expand Down Expand Up @@ -65,13 +71,21 @@ func BuildDependencyTree(params technologies.BuildInfoBomGeneratorParams) (depen
if err != nil {
return
}

// Modules satisfied by a local filesystem 'replace' directive were never published and must not be
// probed against Artifactory during curation-audit.
var localReplaceModules map[string]map[string]bool
if params.IsCurationCmd {
localReplaceModules = getLocalReplaceModules(currentDir)
}

// Parse the dependencies into Xray dependency tree format
rootNode := &xrayUtils.GraphNode{
Id: goPackageTypeIdentifier + rootModuleName,
Nodes: []*xrayUtils.GraphNode{},
}
uniqueDepsSet := datastructures.MakeSet[string]()
populateGoDependencyTree(rootNode, dependenciesGraph, dependenciesList, uniqueDepsSet)
populateGoDependencyTree(rootNode, dependenciesGraph, dependenciesList, uniqueDepsSet, localReplaceModules)

// In case of curation command, go version is not relevant as it can't be resolved from go repo
if !params.IsCurationCmd {
Expand Down Expand Up @@ -106,26 +120,70 @@ func handleCurationGoError(err error) (bool, error) {
return false, nil
}

func populateGoDependencyTree(currNode *xrayUtils.GraphNode, dependenciesGraph map[string][]string, dependenciesList map[string]bool, uniqueDepsSet *datastructures.Set[string]) {
func populateGoDependencyTree(currNode *xrayUtils.GraphNode, dependenciesGraph map[string][]string, dependenciesList map[string]bool, uniqueDepsSet *datastructures.Set[string], localReplaceModules map[string]map[string]bool) {
if currNode.NodeHasLoop() {
return
}
uniqueDepsSet.Add(currNode.Id)
currDepChildren := dependenciesGraph[strings.TrimPrefix(currNode.Id, goPackageTypeIdentifier)]
// Strip our marker before the graph lookup, so a local module's own (real) dependencies are still found.
graphKey := strings.TrimSuffix(strings.TrimPrefix(currNode.Id, goPackageTypeIdentifier), LocalReplaceMarker)
currDepChildren := dependenciesGraph[graphKey]
Comment thread
Phavya-jfrog marked this conversation as resolved.
// Recursively create & append all node's dependencies.
for _, childName := range currDepChildren {
if !dependenciesList[childName] {
// 'go list all' is more accurate than 'go graph' so we filter out deps that don't exist in go list
continue
}
childId := goPackageTypeIdentifier + childName
if isLocalReplaceModule(childName, localReplaceModules) {
childId += LocalReplaceMarker
}
childNode := &xrayUtils.GraphNode{
Id: goPackageTypeIdentifier + childName,
Id: childId,
Nodes: []*xrayUtils.GraphNode{},
Parent: currNode,
}
currNode.Nodes = append(currNode.Nodes, childNode)
populateGoDependencyTree(childNode, dependenciesGraph, dependenciesList, uniqueDepsSet)
populateGoDependencyTree(childNode, dependenciesGraph, dependenciesList, uniqueDepsSet, localReplaceModules)
}
}

// isLocalReplaceModule reports whether childName ("<path>:<version>") is covered by a local replace. ""
// in the versions set means unconditional (any version); otherwise only that exact pinned version matches.
func isLocalReplaceModule(childName string, localReplaceModules map[string]map[string]bool) bool {
modulePath, version, found := strings.Cut(childName, ":")
if !found {
modulePath = childName
version = ""
}
versions := localReplaceModules[modulePath]
return versions[""] || versions[version]
}

// getLocalReplaceModules returns, per module path, the versions go.mod at projectDir replaces with a local
// directory ("" = unconditional). Module-to-module replaces are excluded. Fails open on read/parse errors.
func getLocalReplaceModules(projectDir string) map[string]map[string]bool {
localReplaceModules := map[string]map[string]bool{}
goModPath := filepath.Join(projectDir, "go.mod")
data, err := os.ReadFile(goModPath)
if err != nil {
log.Warn("curation-audit: failed reading go.mod to detect local 'replace' directives, local modules will not be skipped: " + err.Error())
return localReplaceModules
}
modFile, err := modfile.Parse(goModPath, data, nil)
if err != nil {
log.Warn("curation-audit: failed parsing go.mod to detect local 'replace' directives, local modules will not be skipped: " + err.Error())
return localReplaceModules
}
for _, r := range modFile.Replace {
if modfile.IsDirectoryPath(r.New.Path) {
if localReplaceModules[r.Old.Path] == nil {
localReplaceModules[r.Old.Path] = map[string]bool{}
}
localReplaceModules[r.Old.Path][r.Old.Version] = true
}
}
return localReplaceModules
}

func getGoVersionAsDependency() (*xrayUtils.GraphNode, error) {
Expand Down
Loading
Loading