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
62 changes: 32 additions & 30 deletions pkg/depgraph/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,37 @@ const (
)

const (
FlagFailFast = "fail-fast"
FlagAllProjects = "all-projects"
FlagDev = "dev"
FlagExclude = "exclude"
FlagFile = "file"
FlagDetectionDepth = "detection-depth"
FlagPruneRepeatedSubdependencies = "prune-repeated-subdependencies"
FlagMavenAggregateProject = "maven-aggregate-project"
FlagScanUnmanaged = "scan-unmanaged"
FlagScanAllUnmanaged = "scan-all-unmanaged"
FlagSubProject = "sub-project"
FlagGradleSubProject = "gradle-sub-project"
FlagGradleNormalizeDeps = "gradle-normalize-deps"
FlagAllSubProjects = "all-sub-projects"
FlagConfigurationMatching = "configuration-matching"
FlagConfigurationAttributes = "configuration-attributes"
FlagInitScript = "init-script"
FlagYarnWorkspaces = "yarn-workspaces"
FlagPythonCommand = "command"
FlagPythonSkipUnresolved = "skip-unresolved"
FlagPythonPackageManager = "package-manager"
FlagNPMStrictOutOfSync = "strict-out-of-sync"
FlagNugetAssetsProjectName = "assets-project-name"
FlagNugetPkgsFolder = "packages-folder"
FlagUnmanagedMaxDepth = "max-depth"
FlagIncludeProvenance = "include-provenance"
FlagUseSBOMResolution = "use-sbom-resolution"
FlagPrintEffectiveGraph = "effective-graph"
FlagDotnetRuntimeResolution = "dotnet-runtime-resolution"
FlagDotnetTargetFramework = "dotnet-target-framework"
FlagFailFast = "fail-fast"
FlagAllProjects = "all-projects"
FlagDev = "dev"
FlagExclude = "exclude"
FlagFile = "file"
FlagDetectionDepth = "detection-depth"
FlagPruneRepeatedSubdependencies = "prune-repeated-subdependencies"
FlagMavenAggregateProject = "maven-aggregate-project"
FlagScanUnmanaged = "scan-unmanaged"
FlagScanAllUnmanaged = "scan-all-unmanaged"
FlagSubProject = "sub-project"
FlagGradleSubProject = "gradle-sub-project"
FlagGradleNormalizeDeps = "gradle-normalize-deps"
FlagAllSubProjects = "all-sub-projects"
FlagConfigurationMatching = "configuration-matching"
FlagConfigurationAttributes = "configuration-attributes"
FlagInitScript = "init-script"
FlagYarnWorkspaces = "yarn-workspaces"
FlagPythonCommand = "command"
FlagPythonSkipUnresolved = "skip-unresolved"
FlagPythonPackageManager = "package-manager"
FlagNPMStrictOutOfSync = "strict-out-of-sync"
FlagNugetAssetsProjectName = "assets-project-name"
FlagNugetPkgsFolder = "packages-folder"
FlagUnmanagedMaxDepth = "max-depth"
FlagIncludeProvenance = "include-provenance"
FlagUseSBOMResolution = "use-sbom-resolution"
FlagPrintEffectiveGraph = "effective-graph"
FlagPrintEffectiveGraphWithErrors = "effective-graph-with-errors"
FlagDotnetRuntimeResolution = "dotnet-runtime-resolution"
FlagDotnetTargetFramework = "dotnet-target-framework"
)

func getFlagSet() *pflag.FlagSet {
Expand Down Expand Up @@ -70,6 +71,7 @@ func getFlagSet() *pflag.FlagSet {
flagSet.Bool(FlagIncludeProvenance, false, "Include checksums in purl to support package provenance.")
flagSet.Bool(FlagUseSBOMResolution, false, "Use SBOM resolution instead of legacy CLI.")
flagSet.Bool(FlagPrintEffectiveGraph, false, "Return the pruned dependency graph.")
flagSet.Bool(FlagPrintEffectiveGraphWithErrors, false, "Return errors in the pruned dependency graph output.")
flagSet.Bool(FlagDotnetRuntimeResolution, false, "Required. You must use this option when you test .NET projects using Runtime Resolution Scanning.")
flagSet.String(FlagDotnetTargetFramework, "",
"Optional. You may use this option if your solution contains multiple <TargetFramework> directives. "+
Expand Down
7 changes: 7 additions & 0 deletions pkg/depgraph/legacy_resolution.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ func chooseGraphArgument(config configuration.Configuration) (string, parsers.Ou
return "--print-effective-graph", parsers.NewJSONL()
}

if config.GetBool(FlagPrintEffectiveGraphWithErrors) {
return "--print-effective-graph-with-errors", parsers.NewJSONL()
}

return "--print-graph", parsers.NewPlainText()
}

Expand All @@ -63,6 +67,9 @@ func mapToWorkflowData(depGraphs []parsers.DepGraphOutput) []workflow.Data {
if depGraph.Target != nil {
data.SetMetaData(MetaKeyTarget, string(depGraph.Target))
}
if depGraph.Error != nil {
data.AddError(*depGraph.Error)
}
depGraphList = append(depGraphList, data)
}
return depGraphList
Expand Down
34 changes: 34 additions & 0 deletions pkg/depgraph/legacy_resolution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ var payload string
//go:embed testdata/jsonl_output
var jsonlPayload string

//go:embed testdata/jsonl_error_output
var jsonlErrorPayload string

//go:embed testdata/expected_dep_graph.json
var expectedDepGraph string

Expand Down Expand Up @@ -268,6 +271,37 @@ func Test_LegacyResolution(t *testing.T) {
// assert
assert.ErrorIs(t, err, errNoDepGraphsFound)
})

t.Run("should include errors from dep graphs in workflow data", func(t *testing.T) {
config.Set(FlagPrintEffectiveGraphWithErrors, true)

dataIdentifier := workflow.NewTypeIdentifier(WorkflowID, workflowIDStr)
data := workflow.NewData(
dataIdentifier,
contentTypeJSON,
[]byte(jsonlPayload+"\n"+jsonlErrorPayload))
engineMock.
EXPECT().
InvokeWithConfig(legacyWorkflowID, config).
Return([]workflow.Data{data}, nil).
Times(1)

depGraphs, err := handleLegacyResolution(invocationContextMock, config, &nopLogger)
require.Nil(t, err)
require.Len(t, depGraphs, 2)

verifyMeta(t, depGraphs[0], MetaKeyNormalisedTargetFile, "some normalised target file")
verifyMeta(t, depGraphs[0], MetaKeyTargetFileFromPlugin, "some target file from plugin")
verifyMeta(t, depGraphs[0], MetaKeyTarget, `{"key":"some target value"}`)

// verify error
verifyMeta(t, depGraphs[1], MetaKeyNormalisedTargetFile, "some normalised target file")
errorList := depGraphs[1].GetErrorList()
require.Len(t, errorList, 1)
assert.Equal(t, "SNYK-TEST-001", errorList[0].ID)
assert.Equal(t, "Test Error", errorList[0].Title)
assert.Equal(t, "Something went wrong", errorList[0].Detail)
})
}

func invokeWithConfigAndGetTestCmdArgs(
Expand Down
3 changes: 3 additions & 0 deletions pkg/depgraph/parsers/depgraph_output.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package parsers

import "github.com/snyk/error-catalog-golang-public/snyk_errors"

// DepGraphOutput represents a parsed dependency graph output.
type DepGraphOutput struct {
NormalisedTargetFile string
TargetFileFromPlugin *string
Target []byte
DepGraph []byte
Error *snyk_errors.Error
}
12 changes: 8 additions & 4 deletions pkg/depgraph/parsers/jsonl_output_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"bytes"
"encoding/json"
"fmt"

"github.com/snyk/error-catalog-golang-public/snyk_errors"
)

// JSONLOutputParser parses JSONL formatted dependency graph output.
Expand All @@ -18,10 +20,11 @@ func NewJSONL() OutputParser {
}

type jsonLine struct {
DepGraph json.RawMessage `json:"depGraph"`
NormalisedTargetFile string `json:"normalisedTargetFile"`
TargetFileFromPlugin *string `json:"targetFileFromPlugin"`
Target json.RawMessage `json:"target"`
DepGraph json.RawMessage `json:"depGraph"`
NormalisedTargetFile string `json:"normalisedTargetFile"`
TargetFileFromPlugin *string `json:"targetFileFromPlugin"`
Target json.RawMessage `json:"target"`
Error *snyk_errors.Error `json:"error"`
}

// ParseOutput parses JSONL formatted dependency graph output.
Expand All @@ -47,6 +50,7 @@ func (j *JSONLOutputParser) ParseOutput(data []byte) ([]DepGraphOutput, error) {
TargetFileFromPlugin: parsed.TargetFileFromPlugin,
Target: parsed.Target,
DepGraph: parsed.DepGraph,
Error: parsed.Error,
})
}

Expand Down
68 changes: 59 additions & 9 deletions pkg/depgraph/sbom_resolution.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,19 +136,30 @@ func handleSBOMResolutionDI(
workflowData = append(workflowData, data)
}

totalFindings := len(findings)

if len(findings) == 0 || allProjects {
applyFindingsExclusions(config, findings)

legacyData, err := executeLegacyWorkflow(ctx, config, logger, depGraphWorkflowFunc, findings)
if err != nil {
return nil, err
}
if legacyData != nil {
workflowData = append(workflowData, legacyData...)
}

legacyWorkflowData, legacyProblemFindings := processLegacyData(logger, legacyData)
workflowData = append(workflowData, legacyWorkflowData...)
problemFindings = append(problemFindings, legacyProblemFindings...)

totalFindings += len(legacyData)
}

outputAnyWarnings(ctx, logger, problemFindings)
// TODO: This is a temporary implementation for rendering warnings.
// The long-term plan is for the CLI to handle all warning rendering.
// This will require extensions to handle `workflow.Data` objects with
// errors and propagate them upstream rather than rendering them directly.
// This change will require coordinated updates across extensions to
// ensure backwards compatibility and avoid breakages.
outputAnyWarnings(ctx, logger, problemFindings, totalFindings)

return workflowData, nil
}
Expand All @@ -162,9 +173,9 @@ func logFindingError(logger *zerolog.Logger, lockFile string, err error) {
}
}

func outputAnyWarnings(ctx workflow.InvocationContext, logger *zerolog.Logger, problemFindings []scaplugin.Finding) {
func outputAnyWarnings(ctx workflow.InvocationContext, logger *zerolog.Logger, problemFindings []scaplugin.Finding, totalFindings int) {
if len(problemFindings) > 0 {
message := renderWarningForProblemFindings(problemFindings)
message := renderWarningForProblemFindings(problemFindings, totalFindings)

err := ctx.GetUserInterface().Output(message + "\n")
if err != nil {
Expand All @@ -173,7 +184,7 @@ func outputAnyWarnings(ctx workflow.InvocationContext, logger *zerolog.Logger, p
}
}

func renderWarningForProblemFindings(problemFindings []scaplugin.Finding) string {
func renderWarningForProblemFindings(problemFindings []scaplugin.Finding, totalFindings int) string {
outputMessage := ""
for _, finding := range problemFindings {
outputMessage += fmt.Sprintf("\n%s:", finding.LockFile)
Expand All @@ -184,12 +195,47 @@ func renderWarningForProblemFindings(problemFindings []scaplugin.Finding) string
outputMessage += "\n could not process manifest file"
}
}
outputMessage += fmt.Sprintf("\n✗ %d potential projects failed to get dependencies.", len(problemFindings))
outputMessage += fmt.Sprintf("\n✗ %d/%d potential projects failed to get dependencies.", len(problemFindings), totalFindings)

redStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "9", Dark: "1"})
return redStyle.Render(outputMessage)
}

// processLegacyData separates successful dependency graphs from errors in the legacy data.
// It returns workflow data containing only valid dependency graphs, while converting
// any errors into problem findings that can be reported as warnings.
func processLegacyData(logger *zerolog.Logger, legacyData []workflow.Data) ([]workflow.Data, []scaplugin.Finding) {
workflowData := make([]workflow.Data, 0, len(legacyData))
problemFindings := make([]scaplugin.Finding, 0)

for _, data := range legacyData {
errList := data.GetErrorList()
if len(errList) > 0 {
problemFindings = append(problemFindings, extractProblemFindings(logger, data, errList)...)
continue
}
workflowData = append(workflowData, data)
}

return workflowData, problemFindings
}

func extractProblemFindings(logger *zerolog.Logger, data workflow.Data, errList []snyk_errors.Error) []scaplugin.Finding {
findings := make([]scaplugin.Finding, 0, len(errList))
lockFile, metaErr := data.GetMetaData(contentLocationKey)
if metaErr != nil {
logger.Printf("Failed to get metadata %s for workflow data: %v", contentLocationKey, metaErr)
lockFile = "unknown"
}
for i := range errList {
findings = append(findings, scaplugin.Finding{
LockFile: lockFile,
Error: errList[i],
})
}
return findings
}

func getExclusionsFromFindings(findings []scaplugin.Finding) []string {
exclusions := []string{}
for i := range findings {
Expand Down Expand Up @@ -238,7 +284,11 @@ func executeLegacyWorkflow(
depGraphWorkflowFunc ResolutionHandlerFunc,
findings []scaplugin.Finding,
) ([]workflow.Data, error) {
legacyData, err := depGraphWorkflowFunc(ctx, config, logger)
legacyConfig := config.Clone()
legacyConfig.Unset(FlagPrintEffectiveGraph)
legacyConfig.Set(FlagPrintEffectiveGraphWithErrors, true)

legacyData, err := depGraphWorkflowFunc(ctx, legacyConfig, logger)
if err == nil {
return legacyData, nil
}
Expand Down
Loading