Skip to content

Commit adfc017

Browse files
fix: support both sarif and json file output for native code implementation (#375)
Co-authored-by: Peter Schäfer <[email protected]>
1 parent f556bec commit adfc017

File tree

7 files changed

+283
-112
lines changed

7 files changed

+283
-112
lines changed

pkg/local_workflows/output_workflow.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ func InitOutputWorkflow(engine workflow.Engine) error {
3131
outputConfig.Bool(configuration.FLAG_INCLUDE_IGNORES, false, "Include ignored findings in the output")
3232
outputConfig.String(configuration.FLAG_SEVERITY_THRESHOLD, "low", "Severity threshold for findings to be included in the output")
3333

34-
// set default values for configuration values
35-
engine.GetConfiguration().AddDefaultValue(output_workflow.OUTPUT_CONFIG_WRITE_EMPTY_FILE, configuration.StandardDefaultValueFunction(true))
36-
3734
entry, err := engine.Register(WORKFLOWID_OUTPUT_WORKFLOW, workflow.ConfigurationOptionsFromFlagset(outputConfig), outputWorkflowEntryPointImpl)
3835
entry.SetVisibility(false)
3936

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
package output_workflow
22

3+
import "github.com/snyk/go-application-framework/internal/presenters"
4+
35
const (
46
OUTPUT_CONFIG_KEY_JSON = "json"
57
OUTPUT_CONFIG_KEY_JSON_FILE = "json-file-output"
68
OUTPUT_CONFIG_KEY_SARIF = "sarif"
79
OUTPUT_CONFIG_KEY_SARIF_FILE = "sarif-file-output"
810
OUTPUT_CONFIG_TEMPLATE_FILE = "internal_template_file"
9-
OUTPUT_CONFIG_WRITE_EMPTY_FILE = "internal_write_empty_file"
11+
OUTPUT_CONFIG_KEY_FILE_WRITERS = "internal_output_file_writers"
1012
DEFAULT_WRITER = "default"
13+
DEFAULT_MIME_TYPE = presenters.DefaultMimeType
14+
SARIF_MIME_TYPE = presenters.ApplicationSarifMimeType
1115
)
16+
17+
// DefaultTemplateFiles is an instance of TemplatePathsStruct with the template paths.
18+
var DefaultTemplateFiles = presenters.DefaultTemplateFiles
19+
var ApplicationSarifTemplates = presenters.ApplicationSarifTemplates
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package output_workflow
2+
3+
import (
4+
"io"
5+
"os"
6+
7+
iUtils "github.com/snyk/go-application-framework/internal/utils"
8+
)
9+
10+
type delayedFileOpenWriteCloser struct {
11+
Filename string
12+
file io.WriteCloser
13+
}
14+
15+
func (wc *delayedFileOpenWriteCloser) Write(p []byte) (n int, err error) {
16+
// lazy open file if not exists
17+
if wc.file == nil {
18+
pathError := iUtils.CreateFilePath(wc.Filename)
19+
if pathError != nil {
20+
return 0, pathError
21+
}
22+
23+
file, fileErr := os.OpenFile(wc.Filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, iUtils.FILEPERM_666)
24+
if fileErr != nil {
25+
return 0, fileErr
26+
}
27+
28+
wc.file = file
29+
}
30+
31+
return wc.file.Write(p)
32+
}
33+
34+
func (wc *delayedFileOpenWriteCloser) Close() error {
35+
if wc.file != nil {
36+
return wc.file.Close()
37+
}
38+
return nil
39+
}

pkg/local_workflows/output_workflow/findings_model_tools.go

Lines changed: 82 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"encoding/json"
66
"fmt"
77
"io"
8-
"os"
98
"strings"
109

1110
"github.com/rs/zerolog"
@@ -19,11 +18,20 @@ import (
1918
"github.com/snyk/go-application-framework/pkg/workflow"
2019
)
2120

21+
// WriterEntry is an internal structure to handle all template based writers
2222
type WriterEntry struct {
23-
writer io.Writer
24-
mimeType string
25-
templates []string
26-
closer func() error
23+
writer io.WriteCloser
24+
mimeType string
25+
templates []string
26+
renderEmptyData bool
27+
}
28+
29+
// FileWriter is a public structure used to configure file based rendering
30+
type FileWriter struct {
31+
NameConfigKey string // defines the configuration key to look up the filename under
32+
MimeType string // defines the final mime type of the rendering
33+
TemplateFiles []string // defines the set of template files to use for rendering
34+
WriteEmptyContent bool // specifies if anything should be written at all if the input data is empty
2735
}
2836

2937
func getListOfFindings(input []workflow.Data, debugLogger *zerolog.Logger) (findings []*local_models.LocalFinding, remainingData []workflow.Data) {
@@ -65,83 +73,89 @@ func getTotalNumberOfFindings(findings []*local_models.LocalFinding) uint32 {
6573
return count
6674
}
6775

68-
func getWritersToUse(config configuration.Configuration, outputDestination iUtils.OutputDestination, findings []*local_models.LocalFinding) (map[string]*WriterEntry, error) {
76+
func getWritersToUse(config configuration.Configuration, outputDestination iUtils.OutputDestination) map[string]*WriterEntry {
77+
// resulting map of writers and their templates
6978
writerMap := map[string]*WriterEntry{
70-
DEFAULT_WRITER: {
71-
writer: outputDestination.GetWriter(),
72-
mimeType: presenters.DefaultMimeType,
73-
templates: presenters.DefaultTemplateFiles,
74-
closer: func() error {
75-
_, err := fmt.Fprintln(outputDestination.GetWriter(), "")
76-
return err
77-
},
78-
},
79+
DEFAULT_WRITER: getDefaultWriter(config, outputDestination),
7980
}
8081

81-
sarifWriter, err := getSarifFileRenderer(config, findings)
82-
if err != nil {
83-
return writerMap, err
82+
// default file writers
83+
fileWriters := []FileWriter{
84+
{
85+
OUTPUT_CONFIG_KEY_SARIF_FILE,
86+
SARIF_MIME_TYPE,
87+
ApplicationSarifTemplates,
88+
true,
89+
},
90+
/*
91+
skipping support for json file output by default, since there is no supporting rendering yet.
92+
{
93+
OUTPUT_CONFIG_KEY_JSON_FILE,
94+
SARIF_MIME_TYPE,
95+
ApplicationSarifTemplates,
96+
true,
97+
},*/
98+
}
99+
100+
// use configured file writers if available
101+
if tmp, ok := config.Get(OUTPUT_CONFIG_KEY_FILE_WRITERS).([]FileWriter); ok {
102+
fileWriters = tmp
103+
}
104+
105+
for _, fileWriter := range fileWriters {
106+
if config.IsSet(fileWriter.NameConfigKey) {
107+
writerMap[fileWriter.NameConfigKey] = &WriterEntry{
108+
writer: &delayedFileOpenWriteCloser{Filename: config.GetString(fileWriter.NameConfigKey)},
109+
mimeType: fileWriter.MimeType,
110+
templates: fileWriter.TemplateFiles,
111+
renderEmptyData: fileWriter.WriteEmptyContent,
112+
}
113+
}
84114
}
85115

86-
if sarifWriter != nil {
87-
writerMap[OUTPUT_CONFIG_KEY_SARIF_FILE] = sarifWriter
116+
return writerMap
117+
}
118+
119+
func getDefaultWriter(config configuration.Configuration, outputDestination iUtils.OutputDestination) *WriterEntry {
120+
writer := &WriterEntry{
121+
writer: &newLineCloser{
122+
writer: outputDestination.GetWriter(),
123+
},
124+
mimeType: DEFAULT_MIME_TYPE,
125+
templates: DefaultTemplateFiles,
126+
renderEmptyData: true,
88127
}
89128

90129
if config.GetBool(OUTPUT_CONFIG_KEY_SARIF) {
91-
writerMap[DEFAULT_WRITER].mimeType = presenters.ApplicationSarifMimeType
92-
writerMap[DEFAULT_WRITER].templates = presenters.ApplicationSarifTemplates
130+
writer.mimeType = SARIF_MIME_TYPE
131+
writer.templates = ApplicationSarifTemplates
93132
}
94133

95134
if config.IsSet(OUTPUT_CONFIG_TEMPLATE_FILE) {
96-
writerMap[DEFAULT_WRITER].templates = []string{config.GetString(OUTPUT_CONFIG_TEMPLATE_FILE)}
135+
writer.templates = []string{config.GetString(OUTPUT_CONFIG_TEMPLATE_FILE)}
97136
}
98137

99-
return writerMap, nil
138+
return writer
100139
}
101140

102-
func getSarifFileRenderer(config configuration.Configuration, findings []*local_models.LocalFinding) (*WriterEntry, error) {
103-
outputFileName := config.GetString(OUTPUT_CONFIG_KEY_SARIF_FILE)
104-
if len(outputFileName) == 0 {
105-
//nolint:nilnil // returning a nil writer is a valid case based on the configuration and is not an error case
106-
return nil, nil
107-
}
108-
109-
if !config.GetBool(OUTPUT_CONFIG_WRITE_EMPTY_FILE) && getTotalNumberOfFindings(findings) == 0 {
110-
//nolint:nilnil // returning a nil writer is a valid case based on the configuration and is not an error case
111-
return nil, nil
112-
}
113-
114-
pathError := iUtils.CreateFilePath(outputFileName)
115-
if pathError != nil {
116-
return nil, pathError
117-
}
118-
119-
file, fileErr := os.OpenFile(outputFileName, os.O_WRONLY|os.O_CREATE, 0644)
120-
if fileErr != nil {
121-
return nil, fileErr
122-
}
141+
func useRendererWith(name string, wEntry *WriterEntry, findings []*local_models.LocalFinding, invocation workflow.InvocationContext) {
142+
debugLogger := invocation.GetEnhancedLogger()
123143

124-
writer := &WriterEntry{
125-
writer: file,
126-
mimeType: presenters.ApplicationSarifMimeType,
127-
templates: presenters.ApplicationSarifTemplates,
128-
closer: func() error { return file.Close() },
144+
if !wEntry.renderEmptyData && getTotalNumberOfFindings(findings) == 0 {
145+
debugLogger.Info().Msgf("[%s] The input is empty, skipping rendering!", name)
146+
return
129147
}
130-
return writer, nil
131-
}
132148

133-
func useRendererWith(name string, wEntry *WriterEntry, debugLogger *zerolog.Logger, findings []*local_models.LocalFinding, config configuration.Configuration, invocation workflow.InvocationContext) {
134149
debugLogger.Info().Msgf("[%s] Creating findings model renderer", name)
135150

136-
if wEntry.closer != nil {
137-
defer func() {
138-
closeErr := wEntry.closer()
139-
if closeErr != nil {
140-
debugLogger.Err(closeErr).Msgf("[%s] Error while closing writer.", name)
141-
}
142-
}()
143-
}
151+
defer func() {
152+
closeErr := wEntry.writer.Close()
153+
if closeErr != nil {
154+
debugLogger.Err(closeErr).Msgf("[%s] Error while closing writer.", name)
155+
}
156+
}()
144157

158+
config := invocation.GetConfiguration()
145159
renderer := presenters.NewLocalFindingsRenderer(
146160
findings,
147161
config,
@@ -160,6 +174,7 @@ func useRendererWith(name string, wEntry *WriterEntry, debugLogger *zerolog.Logg
160174
}
161175

162176
func HandleContentTypeFindingsModel(input []workflow.Data, invocation workflow.InvocationContext, outputDestination iUtils.OutputDestination) ([]workflow.Data, error) {
177+
var err error
163178
debugLogger := invocation.GetEnhancedLogger()
164179
config := invocation.GetConfiguration()
165180

@@ -172,24 +187,22 @@ func HandleContentTypeFindingsModel(input []workflow.Data, invocation workflow.I
172187
threadCount := max(int64(config.GetInt(configuration.MAX_THREADS)), 1)
173188
debugLogger.Info().Msgf("Thread count: %d", threadCount)
174189

175-
writerMap, err := getWritersToUse(config, outputDestination, findings)
176-
if err != nil {
177-
debugLogger.Err(err).Msg("Failed to initialize all required writers")
178-
}
190+
writerMap := getWritersToUse(config, outputDestination)
179191

180192
// iterate over all writers and render for each of them
181193
ctx := context.Background()
182194
availableThreads := semaphore.NewWeighted(threadCount)
183195
for k, v := range writerMap {
184196
err = availableThreads.Acquire(ctx, 1)
185197
if err != nil {
186-
debugLogger.Err(err).Msgf("[%s] Failed to acquire threading lock.", k)
198+
debugLogger.Err(err).Msgf("[%s] Failed to acquire threading lock. Cancel rendering.", k)
199+
break
187200
}
188201

189-
go func() {
202+
go func(name string, writer *WriterEntry) {
190203
defer availableThreads.Release(1)
191-
useRendererWith(k, v, debugLogger, findings, config, invocation)
192-
}()
204+
useRendererWith(name, writer, findings, invocation)
205+
}(k, v)
193206
}
194207

195208
err = availableThreads.Acquire(ctx, threadCount)

0 commit comments

Comments
 (0)