Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8df5279
dd VEX command for handling VEX Reports
refoo0 Oct 8, 2025
f6ea2f0
add vex download function and upstrem column to events
refoo0 Oct 10, 2025
a9e2b0f
add upstream URLs to Artifact model and related database migrations
refoo0 Oct 10, 2025
0fd120f
implement artifact update functionality with upstream URL management
refoo0 Oct 13, 2025
7745477
add vex daemon and refactoring
refoo0 Oct 14, 2025
2688657
fix lint
refoo0 Oct 14, 2025
9b067a8
fix lint
refoo0 Oct 14, 2025
d8e53ab
remove debug print statements
refoo0 Oct 14, 2025
fe586a0
replace error returns with logging for VEX report synchronization
refoo0 Oct 14, 2025
2859bf8
add endpoint to list dependency vulns by asset ID without handled ext…
refoo0 Oct 15, 2025
ae1bccc
merge main
refoo0 Oct 16, 2025
0449c3d
Add ParanoiaMode support and sync functionality for dependency vulner…
refoo0 Oct 16, 2025
66b9e10
fix events update
refoo0 Oct 17, 2025
ded376c
fix sync events
Oct 17, 2025
ca32c33
feat: Enhance vulnerability scanning and SBOM management
Oct 21, 2025
476189e
Merge remote-tracking branch 'origin/main' into feature-vex-download
Oct 21, 2025
ded2842
fix mocks
Oct 21, 2025
e94e7ec
fix lint
Oct 21, 2025
4965e42
fix lint
Oct 21, 2025
e94dc29
fix lint
refoo0 Oct 21, 2025
e5ceeaa
add sbom-report handling to Upstream links
refoo0 Oct 23, 2025
05ec42f
add migration for default node in SBOM
refoo0 Oct 23, 2025
c26e171
fix mocks
refoo0 Oct 23, 2025
29d61be
add migration scripts for artifact links and upstream columns
refoo0 Oct 23, 2025
7e2b31b
fix tests
refoo0 Oct 23, 2025
cf1e805
update upstream type to const
refoo0 Oct 23, 2025
5129c30
add sync issues after updstream update
refoo0 Oct 23, 2025
761d4e8
fix tests
refoo0 Oct 23, 2025
e3a1f43
fix lint
refoo0 Oct 23, 2025
b86681e
merge main
refoo0 Oct 23, 2025
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,3 +414,14 @@ docker run -v "$(PWD):/app" scanner devguard-scanner container-scanning \
--path="/app/image.tar"
```











go run ./cmd/devguard-scanner/main.go sca \ --assetName="org/projects/pro/assets/repo" \--apiUrl="http://localhost:8080" \ --token="f2c1a61bbbd383975c6f3fcfe51033b020b28454ebc6edcb4bf1c310fcce8f97" --path="../my-project-local"
1 change: 1 addition & 0 deletions cmd/devguard-scanner/commands/container_scanning.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func NewContainerScanningCommand() *cobra.Command {

addDependencyVulnsScanFlags(containerScanningCommand)
containerScanningCommand.Flags().String("image", "", "The oci image to scan.")
containerScanningCommand.Flags().String("origin", "container-scan", "The type of the scanner. Can be 'origin' or 'container-scan'. Defaults to 'container-scan'.")

return containerScanningCommand
}
1 change: 1 addition & 0 deletions cmd/devguard-scanner/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ func init() {
NewGetCommand(),
NewCurlCommand(),
NewMergeSBOMSCommand(),
NewVEXCommand(),
)

// Here you will define your flags and configuration settings.
Expand Down
4 changes: 4 additions & 0 deletions cmd/devguard-scanner/commands/sca.go
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,7 @@ func uploadBOM(bom io.Reader) (*http.Response, context.CancelFunc, error) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Scanner", config.RuntimeBaseConfig.ScannerID)
req.Header.Set("X-Artifact-Name", config.RuntimeBaseConfig.ArtifactName)
req.Header.Set("X-Origin", config.RuntimeBaseConfig.Origin)
config.SetXAssetHeaders(req)

resp, err := http.DefaultClient.Do(req)
Expand Down Expand Up @@ -634,5 +635,8 @@ func NewSCACommand() *cobra.Command {
}

addDependencyVulnsScanFlags(scaCommand)
// set default scanner type
scaCommand.PersistentFlags().String("origin", "source-scanner", "The type of the scanner. Can be 'source-scanner' or 'container-scan'. Defaults to 'source-scanner'.")

return scaCommand
}
197 changes: 197 additions & 0 deletions cmd/devguard-scanner/commands/vex.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package commands

import (
"bytes"
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"strings"

"github.com/CycloneDX/cyclonedx-go"
"github.com/l3montree-dev/devguard/cmd/devguard-scanner/config"
"github.com/spf13/cobra"
)

func getContainerFile(ctx context.Context, path string) ([]byte, error) {

//check if in workdir a Dockerfile or Container file exists
dockerFilePath := path + "/Dockerfile"
containerFilePath := path + "/Containerfile"

var file []byte
var err error

//check if a Dockerfile exists
if file, err = os.ReadFile(dockerFilePath); err == nil {
return file, nil
}

//check if a Container file exists
if file, err = os.ReadFile(containerFilePath); err == nil {
return file, nil
}
return nil, fmt.Errorf("no Dockerfile or Container file found in path: %s", path)
}

func getImageFromContainerFile(containerFile []byte) (string, error) {
//split the file by lines
lines := strings.Split(string(containerFile), "\n")
lineArr := []string{}
for _, line := range lines {
// delete all spaces from the line
line = strings.TrimSpace(line)
// check if the line starts with FROM
if len(line) > 4 && line[:4] == "FROM" {
lineArr = append(lineArr, line)
}
}

if len(lineArr) == 0 {
return "", fmt.Errorf("no FROM statement found in container file")
}

//get the last FROM statement
lastFrom := lineArr[len(lineArr)-1]

//split the line by spaces
fromParts := strings.Split(lastFrom, " ")
//check if there are at least 2 parts
if len(fromParts) < 2 {
return "", fmt.Errorf("no image found in FROM statement")
}

image := fromParts[1]

//return the image

return image, nil
}

func getVEX(ctx context.Context, imageRef string) (*cyclonedx.BOM, error) {

var vex *cyclonedx.BOM

attestations, err := getAttestations(imageRef)
if err != nil {
return nil, err
}

for _, attestation := range attestations {
if strings.HasPrefix(attestation["predicateType"].(string), "https://cyclonedx.org/vex") {

if vex != nil {
panic("multiple vex documents found for image")
}

predicate, ok := attestation["predicate"].(map[string]any)
if !ok {
panic("could not parse predicate")
}

// marshal the predicate back to json
predicateBytes, err := json.Marshal(predicate)
if err != nil {
panic(err)
}
vex, err = bomFromBytes(predicateBytes)
if err != nil {
panic(err)
}

//save the vex to a file
filename := "vex-" + strings.ReplaceAll(imageRef, "/", "_") + ".json"
file, err := os.Create(filename)
if err != nil {
slog.Error("could not create vex file", "err", err)
continue
}
defer file.Close()

vexBytes, err := json.MarshalIndent(vex, "", " ")
if err != nil {
slog.Error("could not marshal vex", "err", err)
continue
}

_, err = file.Write(vexBytes)
if err != nil {
slog.Error("could not write vex to file", "err", err)
continue
}

slog.Info("wrote vex to file", "file", file.Name())
}
}

return vex, nil
}

func vexCommand(cmd *cobra.Command, args []string) error {

ctx := cmd.Context()

// check if the is a container file or a dockerfile
containerFile, err := getContainerFile(ctx, config.RuntimeBaseConfig.Path)
if err != nil {
return err
}

//get the last from statement from the container file
imagePath, err := getImageFromContainerFile(containerFile)
if err != nil {
return err
}

//check if there is a vex file for the image
vex, err := getVEX(ctx, imagePath)
if err != nil {
return err
}

//upload the vex file
if vex != nil {
vexBuff := &bytes.Buffer{}
// marshal the bom back to json
err := cyclonedx.NewBOMEncoder(vexBuff, cyclonedx.BOMFileFormatJSON).Encode(vex)
if err != nil {
return err
}

// upload the vex
vexResp, err := uploadVEX(vexBuff)
if err != nil {
slog.Error("could not upload vex", "err", err)
} else {
defer vexResp.Body.Close()
if vexResp.StatusCode != http.StatusOK {
slog.Error("could not upload vex", "status", vexResp.Status)
} else {
slog.Info("uploaded vex successfully")
}
}
} else {
slog.Info("no vex document found for image") //, "image") //imagePath)
}

slog.Info("vex called", "file", vex)

return nil
}

func NewVEXCommand() *cobra.Command {
vexCommand := &cobra.Command{
Use: "vex",
Short: "Commands for working with VEX documents",
Args: cobra.ExactArgs(0),
RunE: vexCommand,
}

addDefaultFlags(vexCommand)
addAssetRefFlags(vexCommand)
vexCommand.Flags().String("path", ".", "The path to the project to scan. Defaults to the current directory.")

return vexCommand
}
1 change: 1 addition & 0 deletions cmd/devguard-scanner/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type baseConfig struct {
DefaultBranch string `json:"defaultRef" mapstructure:"defaultRef"`
IsTag bool `json:"isTag" mapstructure:"isTag"`
ArtifactName string `json:"artifactName" mapstructure:"artifactName"`
Origin string `json:"origin" mapstructure:"origin"`

Offline bool `json:"offline" mapstructure:"offline"`
}
Expand Down
17 changes: 12 additions & 5 deletions cmd/devguard/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,12 @@ func BuildRouter(db core.DB, broker pubsub.Broker) *echo.Echo {
licenseRiskService := vuln.NewLicenseRiskService(licenseRiskRepository, vulnEventRepository)
componentService := component.NewComponentService(&openSourceInsightsService, componentProjectRepository, componentRepository, licenseRiskService, artifactRepository, utils.NewFireAndForgetSynchronizer())

artifactService := artifact.NewService(artifactRepository)
artifactController := artifact.NewController(artifactService)

// release module
// release repository will be created later when project router is available
assetVersionService := assetversion.NewService(assetVersionRepository, componentRepository, dependencyVulnRepository, firstPartyVulnRepository, dependencyVulnService, firstPartyVulnService, assetRepository, projectRepository, orgRepository, vulnEventRepository, &componentService, thirdPartyIntegration, licenseRiskRepository, artifactService)
assetVersionService := assetversion.NewService(assetVersionRepository, componentRepository, dependencyVulnRepository, firstPartyVulnRepository, dependencyVulnService, firstPartyVulnService, assetRepository, projectRepository, orgRepository, vulnEventRepository, &componentService, thirdPartyIntegration, licenseRiskRepository)

artifactService := artifact.NewService(artifactRepository, cveRepository, componentRepository, dependencyVulnRepository, assetRepository, assetVersionRepository, assetVersionService, dependencyVulnService)

statisticsService := statistics.NewService(statisticsRepository, componentRepository, assetRiskAggregationRepository, dependencyVulnRepository, assetVersionRepository, projectRepository, releaseRepository)
invitationRepository := repositories.NewInvitationRepository(db)

Expand All @@ -200,7 +200,9 @@ func BuildRouter(db core.DB, broker pubsub.Broker) *echo.Echo {
externalEntityProviderService := integrations.NewExternalEntityProviderService(projectService, assetService, assetRepository, projectRepository, casbinRBACProvider, orgRepository)

// init all http controllers using the repositories
dependencyVulnController := vuln.NewHTTPController(dependencyVulnRepository, dependencyVulnService, projectService, statisticsService)

artifactController := artifact.NewController(artifactRepository, artifactService, dependencyVulnService)
dependencyVulnController := vuln.NewHTTPController(dependencyVulnRepository, dependencyVulnService, projectService, statisticsService, vulnEventRepository)
vulnEventController := events.NewVulnEventController(vulnEventRepository, assetVersionRepository)
policyController := compliance.NewPolicyController(policyRepository, projectRepository)
patController := pat.NewHTTPController(patRepository)
Expand Down Expand Up @@ -448,6 +450,8 @@ func BuildRouter(db core.DB, broker pubsub.Broker) *echo.Echo {
assetVersionRouter.GET("/events/", vulnEventController.ReadEventsByAssetIDAndAssetVersionName)
assetVersionRouter.GET("/artifacts/", assetVersionController.ListArtifacts)

assetVersionRouter.POST("/artifacts/", artifactController.Create, neededScope([]string{"manage"}))

assetVersionRouter.POST("/components/licenses/refresh/", assetVersionController.RefetchLicenses, neededScope([]string{"manage"}))
assetVersionRouter.DELETE("/", assetVersionController.Delete, neededScope([]string{"manage"}), assetScopedRBAC(core.ObjectAsset, core.ActionUpdate))

Expand All @@ -461,13 +465,16 @@ func BuildRouter(db core.DB, broker pubsub.Broker) *echo.Echo {
artifactRouter.GET("/sbom.pdf/", assetVersionController.BuildPDFFromSBOM)

artifactRouter.DELETE("/", artifactController.DeleteArtifact, neededScope([]string{"manage"}))
artifactRouter.PUT("/", artifactController.UpdateArtifact, neededScope([]string{"manage"}))

dependencyVulnRouter := assetVersionRouter.Group("/dependency-vulns")
dependencyVulnRouter.GET("/", dependencyVulnController.ListPaged)
dependencyVulnRouter.GET("/sync/", dependencyVulnController.ListByAssetIDWithoutHandledExternalEventsPaged)
dependencyVulnRouter.GET("/:dependencyVulnID/", dependencyVulnController.Read)
dependencyVulnRouter.GET("/:dependencyVulnID/events/", vulnEventController.ReadAssetEventsByVulnID)
dependencyVulnRouter.GET("/:dependencyVulnID/hints/", dependencyVulnController.Hints)

dependencyVulnRouter.POST("/sync/", dependencyVulnController.SyncDependencyVulns, neededScope([]string{"manage"}))
dependencyVulnRouter.POST("/:dependencyVulnID/", dependencyVulnController.CreateEvent, neededScope([]string{"manage"}))
dependencyVulnRouter.POST("/:dependencyVulnID/mitigate/", dependencyVulnController.Mitigate, neededScope([]string{"manage"}))

Expand Down
Loading