Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1b0c30b
7989: Add Support for Parsing SPDX JSON Files in Dependency Parser - …
suraj-jadhav-ot Jun 25, 2026
2b145af
7989: Add Support for Parsing SPDX JSON Files in Dependency Parser - …
suraj-jadhav-ot Jun 25, 2026
164ed72
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 25, 2026
584f80e
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 25, 2026
43658e8
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 29, 2026
4d8bc28
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 29, 2026
f9d61ff
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 29, 2026
40a615c
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 29, 2026
952e363
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 29, 2026
4fa7d74
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 29, 2026
ec8b292
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 29, 2026
7746461
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 29, 2026
4f1087c
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 29, 2026
eae6cb3
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 29, 2026
9b241b5
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 29, 2026
133939a
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 29, 2026
e8d62b5
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 29, 2026
98548cc
7973: Add Dart/pub CLI parsing support (pubspec.lock + pubspec.yaml)
suraj-jadhav-ot Jun 29, 2026
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
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ jobs:
npm install --global bower
bower -v

- name: Install Dart SDK
uses: dart-lang/setup-dart@v1
with:
sdk: stable

- name: Install PHP & Composer (macOS)
if: runner.os == 'macOS'
shell: bash
Expand Down
26 changes: 10 additions & 16 deletions build/docker/debian.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ RUN echo "deb http://deb.debian.org/debian unstable main" >> /etc/apt/sources.li
# echo "Pin: release a=testing" >> /etc/apt/preferences && \
# echo "Pin-Priority: -3" >> /etc/apt/preferences

RUN apt -y update && apt -y upgrade && apt -y install curl gnupg unzip && \
RUN apt -y update && apt -y install curl gnupg unzip && \
apt -y clean && rm -rf /var/lib/apt/lists/*

RUN mkdir -p /etc/apt/keyrings
Expand All @@ -73,7 +73,7 @@ RUN curl -fsSLO https://services.gradle.org/distributions/gradle-$GRADLE_VERSION
ENV NODE_MAJOR="20"
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.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 upgrade && apt -y install nodejs && \
RUN apt -y update && apt -y install nodejs && \
apt -y clean && rm -rf /var/lib/apt/lists/*
RUN npm install --global npm@latest && \
npm install --global yarn && \
Expand All @@ -96,26 +96,20 @@ RUN curl -fsSLO https://dot.net/v1/dotnet-install.sh \
&& rm ./dotnet-install.sh \
&& dotnet help

# Prevent systemd from being configured to avoid QEMU segfaults on arm64
#Prevent systemd from being configured to avoid QEMU segfaults on arm64
RUN echo 'exit 101' > /usr/sbin/policy-rc.d && chmod +x /usr/sbin/policy-rc.d

RUN apt -y update && apt -y install ca-certificates && \
apt-mark hold systemd && \
apt -y -o Dpkg::Options::="--force-overwrite" \
-o Dpkg::Options::="--force-confold" \
-o Dpkg::Options::="--force-confdef" \
install -t unstable --no-install-recommends \
python3.13 \
python3.13-venv \
apt -y install --no-install-recommends \
python3 \
python3-venv \
python3-pip \
openjdk-21-jdk && \
apt -y clean && rm -rf /var/lib/apt/lists/* && \
ln -s /usr/bin/python3.13 /usr/bin/python
openjdk-17-jdk

RUN dotnet --version && go version

RUN apt update -y && \
apt install -t unstable lsb-release apt-transport-https ca-certificates software-properties-common -y && \
apt install lsb-release apt-transport-https ca-certificates software-properties-common -y && \
curl -o /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg && \
sh -c 'echo "deb https://packages.sury.org/php/ bookworm main" > /etc/apt/sources.list.d/php.list' && \
apt -y clean && rm -rf /var/lib/apt/lists/*
Expand All @@ -141,7 +135,7 @@ RUN apt -y update && apt -y install \

RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer

RUN ln -sf /usr/bin/python3.13 /usr/bin/python3 && php -v && composer --version && python3 --version
RUN php -v && composer --version && python3 --version

# Install Poetry for Python resolution (pyproject.toml)
RUN curl -sSL https://install.python-poetry.org | python3 - && \
Expand All @@ -157,4 +151,4 @@ ENV PATH="/root/.local/bin:$PATH"
CMD [ "debricked", "scan" ]

# Put copy at the end to speedup Docker build by caching previous RUNs and run those concurrently
COPY --from=dev /cli/debricked /usr/bin/debricked
COPY --from=dev /cli/debricked /usr/bin/debricked
2 changes: 2 additions & 0 deletions internal/resolution/pm/pm.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/debricked/cli/internal/resolution/pm/pip"
"github.com/debricked/cli/internal/resolution/pm/pnpm"
"github.com/debricked/cli/internal/resolution/pm/poetry"
"github.com/debricked/cli/internal/resolution/pm/pub"
"github.com/debricked/cli/internal/resolution/pm/sbt"
"github.com/debricked/cli/internal/resolution/pm/uv"
"github.com/debricked/cli/internal/resolution/pm/yarn"
Expand All @@ -36,5 +37,6 @@ func Pms() []IPm {
nuget.NewPm(),
composer.NewPm(),
sbt.NewPm(),
pub.NewPm(),
}
}
1 change: 1 addition & 0 deletions internal/resolution/pm/pm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func TestPms(t *testing.T) {
"go",
"gradle",
"composer",
"pub",
}

for _, pmName := range pmNames {
Expand Down
65 changes: 65 additions & 0 deletions internal/resolution/pm/pub/cmd_factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package pub

import (
"os"
"os/exec"
"path/filepath"
)

type ICmdFactory interface {
MakeLockCmd(manifestFile string) (*exec.Cmd, error)
MakeDepsCmd(manifestFile string) (*exec.Cmd, error)
}

type IExecPath interface {
LookPath(file string) (string, error)
}

type ExecPath struct{}

func (_ ExecPath) LookPath(file string) (string, error) {
return exec.LookPath(file)
}

type CmdFactory struct {
execPath IExecPath
}

// MakeLockCmd creates an exec.Cmd that runs `dart pub get` in the directory
// of the given manifest file. This resolves all dependencies and writes a
// pubspec.lock file that can be used for SCA scanning.
func (cmdf CmdFactory) MakeLockCmd(manifestFile string) (*exec.Cmd, error) {
dartPath, err := cmdf.execPath.LookPath("dart")
if err != nil {
return nil, err
}

workingDir := filepath.Dir(filepath.Clean(manifestFile))

return &exec.Cmd{
Path: dartPath,
Args: []string{"dart", "pub", "get"},
Dir: workingDir,
Env: os.Environ(),
}, nil
}

// MakeDepsCmd creates an exec.Cmd that runs `dart pub deps --json` in the
// directory of the given manifest file. This outputs explicit parent-child
// dependency relationships used to reconstruct the transitive dependency tree.
func (cmdf CmdFactory) MakeDepsCmd(manifestFile string) (*exec.Cmd, error) {
dartPath, err := cmdf.execPath.LookPath("dart")
if err != nil {
return nil, err
}

workingDir := filepath.Dir(filepath.Clean(manifestFile))

return &exec.Cmd{

Path: dartPath,
Args: []string{"dart", "pub", "deps", "--json"},
Dir: workingDir,
Env: os.Environ(),
}, nil
}
43 changes: 43 additions & 0 deletions internal/resolution/pm/pub/cmd_factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package pub

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

type execPathMock struct{}

func (execPathMock) LookPath(file string) (string, error) {
return "/usr/bin/" + file, nil
}

func TestMakeLockCmd(t *testing.T) {
factory := CmdFactory{execPath: execPathMock{}}
manifest := filepath.Join("some", "path", "pubspec.yaml")

cmd, err := factory.MakeLockCmd(manifest)
assert.NoError(t, err)
assert.NotNil(t, cmd)
assert.Equal(t, "/usr/bin/dart", cmd.Path)
assert.Contains(t, cmd.Args, "dart")
assert.Contains(t, cmd.Args, "pub")
assert.Contains(t, cmd.Args, "get")
assert.Equal(t, filepath.Dir(manifest), cmd.Dir)
}

func TestMakeDepsCmd(t *testing.T) {
factory := CmdFactory{execPath: execPathMock{}}
manifest := filepath.Join("some", "path", "pubspec.yaml")

cmd, err := factory.MakeDepsCmd(manifest)
assert.NoError(t, err)
assert.NotNil(t, cmd)
assert.Equal(t, "/usr/bin/dart", cmd.Path)
assert.Contains(t, cmd.Args, "dart")
assert.Contains(t, cmd.Args, "pub")
assert.Contains(t, cmd.Args, "deps")
assert.Contains(t, cmd.Args, "--json")
assert.Equal(t, filepath.Dir(manifest), cmd.Dir)
}
120 changes: 120 additions & 0 deletions internal/resolution/pm/pub/job.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package pub

import (
"os"
"regexp"
"strings"

"github.com/debricked/cli/internal/resolution/job"
"github.com/debricked/cli/internal/resolution/pm/util"
)

const (
executableNotFoundErrRegex = `executable file not found`
depsFileName = "pubspec.deps.json"
)

// Job runs `dart pub get` and `dart pub deps --json` for a given pubspec.yaml.
// It produces pubspec.lock and pubspec.deps.json, where the latter contains
// explicit parent-child relationships for full transitive tree reconstruction.
type Job struct {
job.BaseJob
cmdFactory ICmdFactory
}

func NewJob(file string, cmdFactory ICmdFactory) *Job {
return &Job{
BaseJob: job.NewBaseJob(file),
cmdFactory: cmdFactory,
}
}

func (j *Job) Run() {
status := "generating pubspec.lock"
j.SendStatus(status)

lockCmd, err := j.cmdFactory.MakeLockCmd(j.GetFile())
if err != nil {
j.handleError(j.createError(err.Error(), "", status))

return
}

if output, err := lockCmd.Output(); err != nil {
exitErr := j.GetExitError(err, string(output))
errorMessage := strings.Join([]string{string(output), exitErr.Error()}, "")
j.handleError(j.createError(errorMessage, lockCmd.String(), status))

return
}

status = "generating pubspec.deps.json"
j.SendStatus(status)

depsCmd, err := j.cmdFactory.MakeDepsCmd(j.GetFile())
if err != nil {
j.handleError(j.createError(err.Error(), "", status))

return
}

depsOutput, err := depsCmd.Output()
if err != nil {
exitErr := j.GetExitError(err, string(depsOutput))
errorMessage := strings.Join([]string{string(depsOutput), exitErr.Error()}, "")
j.handleError(j.createError(errorMessage, depsCmd.String(), status))

return
}

status = "writing pubspec.deps.json"
j.SendStatus(status)

err = os.WriteFile(util.MakePathFromManifestFile(j.GetFile(), depsFileName), depsOutput, 0600)
if err != nil {
j.handleError(j.createError(err.Error(), "", status))

return
}
}

func (j *Job) createError(errorStr string, cmd string, status string) job.IError {
cmdError := util.NewPMJobError(errorStr)
cmdError.SetCommand(cmd)
cmdError.SetStatus(status)

return cmdError
}

func (j *Job) handleError(cmdError job.IError) {
expressions := []string{
executableNotFoundErrRegex,
}

for _, expression := range expressions {
regex := regexp.MustCompile(expression)
matches := regex.FindAllStringSubmatch(cmdError.Error(), -1)

if len(matches) > 0 {
cmdError = j.addDocumentation(expression, matches, cmdError)
j.Errors().Append(cmdError)

return
}
}

j.Errors().Append(cmdError)
}

func (j *Job) addDocumentation(expr string, _ [][]string, cmdError job.IError) job.IError {
documentation := cmdError.Documentation()

switch expr {
case executableNotFoundErrRegex:
documentation = j.GetExecutableNotFoundErrorDocumentation("Dart")
}

cmdError.SetDocumentation(documentation)

return cmdError
}
66 changes: 66 additions & 0 deletions internal/resolution/pm/pub/job_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package pub

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

jobTestdata "github.com/debricked/cli/internal/resolution/job/testdata"
"github.com/debricked/cli/internal/resolution/pm/pub/testdata"
"github.com/stretchr/testify/assert"
)

func TestNewJob(t *testing.T) {
j := NewJob("file", testdata.CmdFactoryMock{})
assert.Equal(t, "file", j.GetFile())
assert.False(t, j.Errors().HasError())
}

func TestRunCmdErrExecutableNotFound(t *testing.T) {
execErr := errors.New("exec: \"dart\": executable file not found in $PATH")
j := NewJob("file", testdata.CmdFactoryMock{LockErr: execErr})

go jobTestdata.WaitStatus(j)

j.Run()

errs := j.Errors().GetAll()
assert.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "executable file not found")
assert.Contains(t, errs[0].Documentation(), "Dart wasn't found")
}

func TestRunDepsCmdErrExecutableNotFound(t *testing.T) {
execErr := errors.New("exec: \"dart\": executable file not found in $PATH")
j := NewJob("file", testdata.CmdFactoryMock{Name: "echo", Arg: "ok", DepsErr: execErr})

go jobTestdata.WaitStatus(j)

j.Run()

errs := j.Errors().GetAll()
assert.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "executable file not found")
assert.Contains(t, errs[0].Documentation(), "Dart wasn't found")
}

func TestRunSuccess(t *testing.T) {
tmpDir := t.TempDir()
manifest := filepath.Join(tmpDir, "pubspec.yaml")

err := os.WriteFile(manifest, []byte("name: test"), 0600)
assert.NoError(t, err)

j := NewJob(manifest, testdata.CmdFactoryMock{Name: "echo", Arg: "ok"})

go jobTestdata.WaitStatus(j)

j.Run()

assert.False(t, j.Errors().HasError())
assert.Len(t, j.Errors().GetAll(), 0)

_, statErr := os.Stat(filepath.Join(tmpDir, "pubspec.deps.json"))
assert.NoError(t, statErr)
}
Loading
Loading