Skip to content
Open
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
9 changes: 9 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ jobs:
go-version: '1.23.0'
cache: true

- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'

- name: Build embedded SootUp wrapper
run: make build-sootup-jar-embed

- name: Import GPG signing key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v6
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ jobs:
strategy:
matrix:
os: [ 'ubuntu-latest', 'macos-latest', 'windows-latest' ]
java: [11, 17, 20]
java: [11, 17, 21]
fail-fast: false
runs-on: ${{ matrix.os }}

Expand Down Expand Up @@ -174,7 +174,7 @@ jobs:
run: |
cd cmd/debricked
go generate -v -x
- name: Install golangci-lint
run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2
- name: golangci-lint
uses: golangci/golangci-lint-action@v4
with:
version: v1.62
run: $(go env GOPATH)/bin/golangci-lint run ./...
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ internal/scan/testdata/npm/result.json
/internal/file/testdata/misc/yarn.lock
/internal/callgraph/finder/javafinder/testdata/guava/maven.debricked.lock
/internal/resolution/pm/maven/testdata/guava/maven.debricked.lock
build/sootup/SootUpWrapper/target/
15 changes: 15 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ test:
test-e2e:
bash scripts/test_e2e.sh $(type)

# Build the SootUp fat JAR from source.
# The built artifact is placed in:
# build/sootup/SootUpWrapper/target/sootup-wrapper-1.0.0.jar
.PHONY: build-sootup-jar
build-sootup-jar:
cd build/sootup/SootUpWrapper && mvn clean package -q

# Build the SootUp fat JAR AND copy it into the Go CLI embedded directory so
# that it is picked up by the //go:embed directive in sootup_handler.go.
.PHONY: build-sootup-jar-embed
build-sootup-jar-embed: build-sootup-jar
cp build/sootup/SootUpWrapper/target/sootup-wrapper-1.0.0.jar \
internal/callgraph/language/java/embedded/SootUpWrapper.jar
@echo "SootUpWrapper.jar embedded at internal/callgraph/language/java/embedded/SootUpWrapper.jar"

.PHONY: test-e2e-docker
docker-build-dev:
docker build -f build/docker/alpine.Dockerfile -t debricked/cli:dev --target dev .
Expand Down
2 changes: 1 addition & 1 deletion build/docker/debian.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg -
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
RUN apt -y update && apt -y install nodejs && \
apt -y clean && rm -rf /var/lib/apt/lists/*
RUN npm install --global npm@latest && \
RUN npm install --global npm@10 && \
npm install --global yarn && \
npm install --global pnpm && \
npm install --global bower
Expand Down
25 changes: 18 additions & 7 deletions internal/callgraph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@

The Debricked CLI can generate static callgraphs for projects to enable reachability analysis for vulnerabilities.

For the only currently supported language, Java, some setup is required for callgraph generation to work
properly. For more information on this see the Language Support section below.

## Language Support
Debricked CLI callgraph generation currently only supports Java, the specific
documentation for the Java callgraph generation can be
found [here](https://github.com/debricked/cli/blob/main/internal/callgraph/language/java11/README.md).
Debricked CLI callgraph generation currently supports Java and Go.

Java callgraph generation requires compiled classes and supports both the `soot`
and `sootup` engines. For command usage and setup details, see the full CLI
documentation:
https://docs.debricked.com/tools-and-integrations/cli/debricked-cli

## Java engine behavior notes

- `soot` and `sootup` are both supported, but output size is not expected to be
identical between engines.
- `sootup` can produce a smaller graph due to more targeted traversal.
- `sootup` invokedynamic placeholder signatures (`sootup.dummy.InvokeDynamic`)
are filtered from the exported callgraph payload.
- If the SootUp TypeAssigner workaround is triggered, the CLI surfaces which JARs
were excluded from deep analysis during retry.

## Use

Expand All @@ -32,4 +42,5 @@ debricked scan --callgraph
To analyze the generated callgraph it needs to be uploaded using the scan command, either with the
callgraph generation flag as above, or with an already generated call graph by omitting the flag.

For more information see documentation on the specific langauge implementation or see full CLI documentation [here](https://docs.debricked.com/tools-and-integrations/cli/debricked-cli)
For more information, see the full CLI documentation:
https://docs.debricked.com/tools-and-integrations/cli/debricked-cli
41 changes: 40 additions & 1 deletion internal/callgraph/language/java/callgraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package java

import (
"fmt"
"path/filepath"
"regexp"
"strings"

"github.com/debricked/cli/internal/callgraph/cgexec"
ioFs "github.com/debricked/cli/internal/io"
Expand Down Expand Up @@ -103,6 +105,43 @@ func (cg *Callgraph) RunCallGraph(callgraphJarPath string) error {

cmd := cgexec.NewCommand(osCmd)
err = cgexec.RunCommand(*cmd, cg.ctx)
if err != nil {
return err
}

return err
if isSootUpWrapperJar(callgraphJarPath) {
for _, line := range extractSootUpDiagnostics(cmd.GetStdOut().String(), cmd.GetStdErr().String()) {
fmt.Println(line)
}
}

return nil
}

func isSootUpWrapperJar(callgraphJarPath string) bool {
return strings.EqualFold(filepath.Base(callgraphJarPath), "SootUpWrapper.jar")
}

func extractSootUpDiagnostics(stdout string, stderr string) []string {
all := strings.Split(strings.Join([]string{stdout, stderr}, "\n"), "\n")
seen := map[string]struct{}{}
out := []string{}

for _, line := range all {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}

if strings.Contains(trimmed, "TypeAssigner bug in jar") ||
strings.Contains(trimmed, "Excluding jar from deep analysis and retrying") ||
strings.Contains(trimmed, "Call graph succeeded after excluding") {
if _, ok := seen[trimmed]; !ok {
seen[trimmed] = struct{}{}
out = append(out, trimmed)
}
}
}

return out
}
16 changes: 16 additions & 0 deletions internal/callgraph/language/java/callgraph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,19 @@ func TestRunCallGraphErrorMock(t *testing.T) {

assert.NotNil(t, err)
}

func TestIsSootUpWrapperJar(t *testing.T) {
assert.True(t, isSootUpWrapperJar("/tmp/.debricked/SootUpWrapper.jar"))
assert.False(t, isSootUpWrapperJar("/tmp/.debricked/soot-wrapper.jar"))
}

func TestExtractSootUpDiagnostics(t *testing.T) {
stdout := "[SootUpWrapper] Call graph succeeded after excluding 1 problematic jar(s): [foo.jar]\n"
stderr := "[SootUpWrapper] Warning: TypeAssigner bug in jar 'foo.jar' (x)\n Excluding jar from deep analysis and retrying...\n"
lines := extractSootUpDiagnostics(stdout, stderr)

assert.Len(t, lines, 3)
assert.Contains(t, lines[0], "Call graph succeeded after excluding")
assert.Contains(t, lines[1], "TypeAssigner bug in jar")
assert.Contains(t, lines[2], "Excluding jar from deep analysis and retrying")
}
Binary file not shown.
21 changes: 21 additions & 0 deletions internal/callgraph/language/java/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,24 @@ func NewJob(
func (j *Job) Run() {
workingDirectory := j.GetDir()
pmConfig := j.config.PackageManager()
outputName := j.outputName()
targetDir := path.Join(workingDirectory, dependencyDir)

// Resolve compiled classes directory based on package manager
classesPath := workingDirectory
switch pmConfig {
case maven:
classesPath = path.Join(workingDirectory, "target", "classes")
case gradle:
classesPath = path.Join(workingDirectory, "build", "classes", "java", "main")
}

// Use compiled classes path if it exists; otherwise fallback to working directory
targetClasses := []string{workingDirectory}
if _, err := j.fs.Stat(classesPath); !j.fs.IsNotExist(err) {
targetClasses = []string{classesPath}
}

if len(j.GetFiles()) > 0 {
targetClasses = j.GetFiles()
}
Expand Down Expand Up @@ -123,6 +139,7 @@ func (j *Job) runCallGraph(callgraph ICallgraph) {

func (j *Job) runPostProcess() {
workingDirectory := j.GetDir()
outputName := j.outputName()
outputFullPath := path.Join(workingDirectory, outputName)
outputFullPathZip := outputFullPath + ".zip"
j.SendStatus("zipping callgraph")
Expand Down Expand Up @@ -154,3 +171,7 @@ func (j *Job) runPostProcess() {
}
}
}

func (j *Job) outputName() string {
return outputName
}
12 changes: 12 additions & 0 deletions internal/callgraph/language/java/job_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ func TestNewJob(t *testing.T) {
assert.False(t, j.Errors().HasError())
}

func TestOutputNameDefault(t *testing.T) {
config := conf.NewConfig("java", nil, map[string]string{}, true, "maven", "")
j := NewJob(dir, files, testdata.NewEchoCmdFactory(), io.FileWriter{}, io.NewArchive("dir"), config, nil, io.FileSystem{}, testdata.MockSootHandler{})
assert.Equal(t, "debricked-call-graph.java", j.outputName())
}

func TestOutputNameSootUp(t *testing.T) {
config := conf.NewConfig("java", nil, map[string]string{"java-callgraph-engine": "sootup"}, true, "maven", "")
j := NewJob(dir, files, testdata.NewEchoCmdFactory(), io.FileWriter{}, io.NewArchive("dir"), config, nil, io.FileSystem{}, testdata.MockSootHandler{})
assert.Equal(t, "debricked-call-graph.java", j.outputName())
}

func TestRunMakeMavenCopyDependenciesCmdErr(t *testing.T) {
cmdErr := errors.New("cmd-error")
cmdFactoryMock := testdata.NewEchoCmdFactory()
Expand Down
14 changes: 9 additions & 5 deletions internal/callgraph/language/java/soot_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,20 +114,24 @@ func (sh SootHandler) GetSootWrapper(version string, fs ioFs.IFileSystem, arc io
return "", err
}
}
path, err := filepath.Abs(path.Join(debrickedDir, "soot-wrapper.jar"))
absDebrickedDir, err := filepath.Abs(debrickedDir)
if err != nil {
return "", err
}
jarPath, err := filepath.Abs(path.Join(debrickedDir, "soot-wrapper.jar"))
if err != nil {

return "", err
}
if _, err := fs.Stat(path); fs.IsNotExist(err) {
if _, err := fs.Stat(jarPath); fs.IsNotExist(err) {
if version == "21" {
return sh.initializeSootWrapper(fs, debrickedDir)
return sh.initializeSootWrapper(fs, absDebrickedDir)
}

return path, sh.downloadSootWrapper(arc, fs, path, version)
return jarPath, sh.downloadSootWrapper(arc, fs, jarPath, version)
}

return path, nil
return jarPath, nil
}

func (sh SootHandler) getSootHandlerJavaVersion(version int) (string, error) {
Expand Down
57 changes: 57 additions & 0 deletions internal/callgraph/language/java/soot_handler_component_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package java

import (
"fmt"
"path/filepath"
"strings"
"testing"

ioTestData "github.com/debricked/cli/internal/io/testdata"
"github.com/stretchr/testify/assert"
)

func TestSootHandlerComponent_GetSootWrapperInvalidVersionString(t *testing.T) {
h := SootHandler{}
_, err := h.GetSootWrapper("not-a-number", ioTestData.FileSystemMock{}, ioTestData.ArchiveMock{})
assert.EqualError(t, err, "could not convert version to int")
}

func TestSootHandlerComponent_GetSootWrapperUnsupportedJavaVersion(t *testing.T) {
h := SootHandler{}
_, err := h.GetSootWrapper("8", ioTestData.FileSystemMock{}, ioTestData.ArchiveMock{})
assert.EqualError(t, err, "lowest supported version for running callgraph generation is 11")
}

func TestSootHandlerComponent_GetSootWrapperMkdirError(t *testing.T) {
h := SootHandler{}
fsMock := ioTestData.FileSystemMock{
StatError: fmt.Errorf("missing"),
IsNotExistBool: true,
MkdirError: fmt.Errorf("mkdir failed"),
}

_, err := h.GetSootWrapper("11", fsMock, ioTestData.ArchiveMock{})
assert.EqualError(t, err, "mkdir failed")
}

func TestSootHandlerComponent_GetSootWrapperVersion21UsesEmbeddedJar(t *testing.T) {
h := SootHandler{}
fsMock := ioTestData.FileSystemMock{
StatError: fmt.Errorf("missing"),
IsNotExistBool: true,
}

p, err := h.GetSootWrapper("21", fsMock, ioTestData.ArchiveMock{})
assert.NoError(t, err)
assert.NotEmpty(t, p)
assert.True(t, strings.HasSuffix(filepath.ToSlash(p), "/.debricked/SootWrapper.jar"))
}

func TestSootHandlerComponent_GetSootWrapperReturnsExistingPath(t *testing.T) {
h := SootHandler{}
fsMock := ioTestData.FileSystemMock{}

p, err := h.GetSootWrapper("17", fsMock, ioTestData.ArchiveMock{})
assert.NoError(t, err)
assert.True(t, strings.HasSuffix(filepath.ToSlash(p), "/.debricked/soot-wrapper.jar"))
}
6 changes: 4 additions & 2 deletions internal/callgraph/language/java/soot_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (

var sootHandler = SootHandler{}

const createErr = "create error"

func TestInitializeSootWrapper(t *testing.T) {
fsMock := ioTestData.FileSystemMock{}
tempDir, err := fsMock.MkdirTemp(".tmp")
Expand Down Expand Up @@ -68,7 +70,7 @@ func TestDownloadSootWrapperMkdirTempError(t *testing.T) {
}

func TestDownloadSootWrapperCreateError(t *testing.T) {
errString := "create error"
errString := createErr
fsMock := ioTestData.FileSystemMock{CreateError: fmt.Errorf("%s", errString)} //nolint
arcMock := ioTestData.ArchiveMock{}
err := sootHandler.downloadSootWrapper(arcMock, fsMock, "soot-wrapper.jar", "11")
Expand All @@ -77,7 +79,7 @@ func TestDownloadSootWrapperCreateError(t *testing.T) {
}

func TestDownloadSootWrapperUnzipError(t *testing.T) {
errString := "create error"
errString := createErr
fsMock := ioTestData.FileSystemMock{}
arcMock := ioTestData.ArchiveMock{UnzipFileError: fmt.Errorf("%s", errString)} //nolint
err := sootHandler.downloadSootWrapper(arcMock, fsMock, "soot-wrapper.jar", "11")
Expand Down
Loading
Loading