Skip to content

Commit

Permalink
Implement 'BuildDependencyTree' for Conan
Browse files Browse the repository at this point in the history
  • Loading branch information
Igor Zhalkin committed Aug 7, 2024
1 parent 902fdba commit 2af45bb
Show file tree
Hide file tree
Showing 6 changed files with 1,229 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ jobs:
run: python -m pip install pipenv
- name: Setup Poetry
run: python -m pip install poetry
- name: Setup Conan
run: python -m pip install conan
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
with:
Expand Down
143 changes: 143 additions & 0 deletions commands/audit/sca/conan/conan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package conan

import (
"encoding/json"
"errors"
"fmt"
"os/exec"

"github.com/jfrog/gofrog/io"
"github.com/jfrog/jfrog-client-go/utils/errorutils"
xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils"

"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"

"github.com/jfrog/jfrog-client-go/utils/log"

"github.com/jfrog/jfrog-cli-security/utils"
)

const (
PackageTypeIdentifier = "conan://"
)

func BuildDependencyTree(params utils.AuditParams) (dependencyTrees []*xrayUtils.GraphNode, uniqueDeps []string, err error) {
// Prepare
currentDir, err := coreutils.GetWorkingDirectory()
if err != nil {
return
}
conanExecPath, err := getConanExecPath()
if err != nil {
return
}
// Build
return calculateDependencies(conanExecPath, currentDir, params)
}

func getConanExecPath() (conanExecPath string, err error) {
if conanExecPath, err = exec.LookPath("conan"); errorutils.CheckError(err) != nil {
return
}
if conanExecPath == "" {
err = errors.New("could not find the 'conan' executable in the system PATH")
return
}
log.Debug("Using Conan executable:", conanExecPath)
// Validate conan version command
version, err := getConanCmd(conanExecPath, "", "--version").RunWithOutput()
if errorutils.CheckError(err) != nil {
return
}
log.Debug("Conan version:", string(version))
return
}

func getConanCmd(conanExecPath, workingDir, cmd string, args ...string) *io.Command {
command := io.NewCommand(conanExecPath, cmd, args)
command.Dir = workingDir
return command
}

type conanDep struct {
Ref string `json:"ref"`
Direct bool `json:"direct"`
}

type conanRef struct {
Ref string `json:"ref"`
Name string `json:"name"`
Version string `json:"version"`
Dependencies map[string]conanDep `json:"dependencies"`
node *xrayUtils.GraphNode
}

func (cr *conanRef) Node() *xrayUtils.GraphNode {
if cr.node == nil {
cr.node = &xrayUtils.GraphNode{Id: cr.NodeName()}
}
return cr.node
}

func (cr *conanRef) NodeName() string {
return PackageTypeIdentifier + cr.Name + ":" + cr.Version
}

type conanGraphOutput struct {
Graph struct {
Nodes map[string]conanRef `json:"nodes"`
} `json:"graph"`
}

func calculateDependencies(executablePath, workingDir string, params utils.AuditParams) (dependencyTrees []*xrayUtils.GraphNode, uniqueDeps []string, err error) {
graphInfo := append([]string{"info", ".", "--format=json"}, params.Args()...)
conanGraphInfoContent, err := getConanCmd(executablePath, workingDir, "graph", graphInfo...).RunWithOutput()
if err != nil {
return
}

log.Debug("Conan 'graph info' command output:\n", string(conanGraphInfoContent))
var output conanGraphOutput
if err = json.Unmarshal(conanGraphInfoContent, &output); err != nil {
return
}

rootNode, err := parseConanDependencyGraph("0", output.Graph.Nodes)
if err != nil {
return
}
dependencyTrees = append(dependencyTrees, rootNode)

for id, dep := range output.Graph.Nodes {
if id == "0" {
continue
}
uniqueDeps = append(uniqueDeps, dep.NodeName())
}

return
}

func parseConanDependencyGraph(id string, graph map[string]conanRef) (*xrayUtils.GraphNode, error) {
var childrenNodes []*xrayUtils.GraphNode
node, ok := graph[id]
if !ok {
return nil, errors.New(fmt.Sprintf("got non-existant node id %s", id))
}
for key, dep := range node.Dependencies {
if !dep.Direct {
continue
}
r, err := parseConanDependencyGraph(key, graph)
if err != nil {
return nil, err
}
childrenNodes = append(childrenNodes, r)
}
if id == "0" {
return &xrayUtils.GraphNode{Id: "root", Nodes: childrenNodes}, nil
} else {
node.Node().Nodes = childrenNodes
return node.Node(), nil
}
}
58 changes: 58 additions & 0 deletions commands/audit/sca/conan/conan_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package conan

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"

xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils"
"github.com/stretchr/testify/assert"

"github.com/jfrog/jfrog-cli-core/v2/utils/tests"
"github.com/jfrog/jfrog-cli-security/commands/audit/sca"
"github.com/jfrog/jfrog-cli-security/utils"
)

var expectedResult = &xrayUtils.GraphNode{
Id: "root",
Nodes: []*xrayUtils.GraphNode{
{Id: "conan://zlib:1.3.1"},
{Id: "conan://openssl:3.0.9", Nodes: []*xrayUtils.GraphNode{{Id: "conan://zlib:1.3.1"}}},
{Id: "conan://meson:1.4.1", Nodes: []*xrayUtils.GraphNode{{Id: "conan://ninja:1.11.1"}}},
},
}

func TestParseConanDependencyTree(t *testing.T) {
_, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("other", "conan"))
defer cleanUp()
dependenciesJson, err := os.ReadFile("dependencies.json")
assert.NoError(t, err)
output := struct {
Graph struct {
Nodes map[string]conanRef `json:"nodes"`
} `json:"graph"`
}{}

err = json.Unmarshal(dependenciesJson, &output)

graph, err := parseConanDependencyGraph("0", output.Graph.Nodes)
assert.NoError(t, err)
if !tests.CompareTree(expectedResult, graph) {
t.Error(fmt.Sprintf("expected %+v, got: %+v", expectedResult.Nodes, graph))
}
}

func TestBuildDependencyTree(t *testing.T) {
_, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("projects", "package-managers", "conan"))
defer cleanUp()
expectedUniqueDeps := []string{"conan://openssl:3.0.9", "conan://zlib:1.3.1", "conan://meson:1.4.1", "conan://ninja:1.11.1"}
params := &utils.AuditBasicParams{}
graph, uniqueDeps, err := BuildDependencyTree(params)
assert.NoError(t, err)
if !tests.CompareTree(expectedResult, graph[0]) {
t.Error(fmt.Sprintf("expected %+v, got: %+v", expectedResult.Nodes, graph))
}
assert.ElementsMatch(t, uniqueDeps, expectedUniqueDeps, "First is actual, Second is Expected")
}
Loading

0 comments on commit 2af45bb

Please sign in to comment.