Skip to content
Merged
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
19 changes: 17 additions & 2 deletions pkg/depgraph/legacy_resolution.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strconv"

"github.com/rs/zerolog"
"github.com/snyk/error-catalog-golang-public/snyk_errors"
"github.com/snyk/go-application-framework/pkg/configuration"
"github.com/snyk/go-application-framework/pkg/workflow"

Expand Down Expand Up @@ -38,7 +39,7 @@ func handleLegacyResolution(ctx workflow.InvocationContext, config configuration
return nil, errNoDepGraphsFound
}

workflowOutputData := mapToWorkflowData(depGraphs)
workflowOutputData := mapToWorkflowData(depGraphs, logger)
logger.Printf("DepGraph workflow done (extracted %d dependency graphs)", len(workflowOutputData))
return workflowOutputData, nil
}
Expand All @@ -48,10 +49,14 @@ 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()
}

func mapToWorkflowData(depGraphs []parsers.DepGraphOutput) []workflow.Data {
func mapToWorkflowData(depGraphs []parsers.DepGraphOutput, logger *zerolog.Logger) []workflow.Data {
depGraphList := []workflow.Data{}
for _, depGraph := range depGraphs {
data := workflow.NewData(DataTypeID, contentTypeJSON, depGraph.DepGraph)
Expand All @@ -63,6 +68,16 @@ func mapToWorkflowData(depGraphs []parsers.DepGraphOutput) []workflow.Data {
if depGraph.Target != nil {
data.SetMetaData(MetaKeyTarget, string(depGraph.Target))
}
if depGraph.Error != nil {
snykErrors, err := snyk_errors.FromJSONAPIErrorBytes(depGraph.Error)
if err != nil {
logger.Printf("failed to parse error from depgraph output: %v", err)
} else {
for i := range len(snykErrors) {
data.AddError(snykErrors[i])
}
}
}
depGraphList = append(depGraphList, data)
}
return depGraphList
Expand Down
32 changes: 32 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_dep_graph_with_error_output
var jsonlDepGraphWithErrorPayload string

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

Expand Down Expand Up @@ -268,6 +271,35 @@ 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(jsonlDepGraphWithErrorPayload))
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")

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

func invokeWithConfigAndGetTestCmdArgs(
Expand Down
1 change: 1 addition & 0 deletions pkg/depgraph/parsers/depgraph_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ type DepGraphOutput struct {
TargetFileFromPlugin *string
Target []byte
DepGraph []byte
Error []byte
}
2 changes: 2 additions & 0 deletions pkg/depgraph/parsers/jsonl_output_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type jsonLine struct {
NormalisedTargetFile string `json:"normalisedTargetFile"`
TargetFileFromPlugin *string `json:"targetFileFromPlugin"`
Target json.RawMessage `json:"target"`
Error json.RawMessage `json:"error"`
}

// ParseOutput parses JSONL formatted dependency graph output.
Expand All @@ -47,6 +48,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