github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/format/template/encoder.go (about)

     1  package template
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"reflect"
    11  	"text/template"
    12  
    13  	"github.com/Masterminds/sprig/v3"
    14  	"github.com/mitchellh/go-homedir"
    15  
    16  	"github.com/anchore/syft/syft/format/syftjson"
    17  	"github.com/anchore/syft/syft/sbom"
    18  )
    19  
    20  const ID sbom.FormatID = "template"
    21  
    22  type EncoderConfig struct {
    23  	TemplatePath string
    24  	Legacy       bool
    25  	syftjson.EncoderConfig
    26  }
    27  
    28  type encoder struct {
    29  	cfg     EncoderConfig
    30  	funcMap template.FuncMap
    31  }
    32  
    33  func NewFormatEncoder(cfg EncoderConfig) (sbom.FormatEncoder, error) {
    34  	// TODO: revisit this... should no template file be an error or simply render an empty result? or render the json output?
    35  	// Note: do not check for the existence of the template file here, as the default encoder cannot provide one.
    36  	f := sprig.HermeticTxtFuncMap()
    37  	f["getLastIndex"] = func(collection interface{}) int {
    38  		if v := reflect.ValueOf(collection); v.Kind() == reflect.Slice {
    39  			return v.Len() - 1
    40  		}
    41  
    42  		return 0
    43  	}
    44  	// Checks if a field is defined
    45  	f["hasField"] = func(obj interface{}, field string) bool {
    46  		t := reflect.TypeOf(obj)
    47  		_, ok := t.FieldByName(field)
    48  		return ok
    49  	}
    50  
    51  	return encoder{
    52  		cfg: cfg,
    53  		// These are custom functions available to template authors.
    54  		funcMap: f,
    55  	}, nil
    56  }
    57  
    58  func DefaultEncoderConfig() EncoderConfig {
    59  	return EncoderConfig{
    60  		EncoderConfig: syftjson.DefaultEncoderConfig(),
    61  	}
    62  }
    63  
    64  func (e encoder) ID() sbom.FormatID {
    65  	return ID
    66  }
    67  
    68  func (e encoder) Aliases() []string {
    69  	return []string{}
    70  }
    71  
    72  func (e encoder) Version() string {
    73  	return sbom.AnyVersion
    74  }
    75  
    76  func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error {
    77  	if e.cfg.TemplatePath == "" {
    78  		return errors.New("no template file provided")
    79  	}
    80  
    81  	templatePath, err := homedir.Expand(e.cfg.TemplatePath)
    82  	if err != nil {
    83  		return fmt.Errorf("unable to expand path %s", e.cfg.TemplatePath)
    84  	}
    85  
    86  	templateContents, err := os.ReadFile(templatePath)
    87  	if err != nil {
    88  		return fmt.Errorf("unable to get template content: %w", err)
    89  	}
    90  
    91  	tmpl, err := template.New(templatePath).Funcs(e.funcMap).Parse(string(templateContents))
    92  	if err != nil {
    93  		return fmt.Errorf("unable to parse template: %w", err)
    94  	}
    95  
    96  	var doc any
    97  	if e.cfg.Legacy {
    98  		doc = syftjson.ToFormatModel(s, e.cfg.EncoderConfig)
    99  	} else {
   100  		enc, err := syftjson.NewFormatEncoderWithConfig(e.cfg.EncoderConfig)
   101  		if err != nil {
   102  			return fmt.Errorf("unable to create json encoder: %w", err)
   103  		}
   104  
   105  		var buff bytes.Buffer
   106  		if err = enc.Encode(&buff, s); err != nil {
   107  			return fmt.Errorf("unable to encode json: %w", err)
   108  		}
   109  
   110  		deserializedDoc := make(map[string]any)
   111  
   112  		if err = json.Unmarshal(buff.Bytes(), &deserializedDoc); err != nil {
   113  			return fmt.Errorf("unable to unmarshal json for template: %w", err)
   114  		}
   115  
   116  		doc = deserializedDoc
   117  	}
   118  
   119  	return tmpl.Execute(writer, doc)
   120  }