github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/githubactions/parse_workflow.go (about) 1 package githubactions 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "regexp" 8 9 "go.yaml.in/yaml/v3" 10 11 "github.com/anchore/syft/internal/unknown" 12 "github.com/anchore/syft/syft/artifact" 13 "github.com/anchore/syft/syft/file" 14 "github.com/anchore/syft/syft/pkg" 15 "github.com/anchore/syft/syft/pkg/cataloger/generic" 16 ) 17 18 var ( 19 _ generic.Parser = parseWorkflowForActionUsage 20 _ generic.Parser = parseWorkflowForWorkflowUsage 21 ) 22 23 type workflowDef struct { 24 Jobs map[string]workflowJobDef `yaml:"jobs"` 25 } 26 27 type workflowJobDef struct { 28 Uses string `yaml:"uses"` 29 UsesComment string `yaml:"-"` 30 Steps []stepDef `yaml:"steps"` 31 } 32 33 type stepDef struct { 34 Name string `yaml:"name"` 35 Uses string `yaml:"uses"` 36 UsesComment string `yaml:"-"` 37 With struct { 38 Path string `yaml:"path"` 39 Key string `yaml:"key"` 40 } `yaml:"with"` 41 } 42 43 func parseWorkflowForWorkflowUsage(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { 44 contents, errs := io.ReadAll(reader) 45 if errs != nil { 46 return nil, nil, fmt.Errorf("unable to read yaml workflow file: %w", errs) 47 } 48 49 // parse the yaml file into a generic node to preserve comments 50 var node yaml.Node 51 if errs = yaml.Unmarshal(contents, &node); errs != nil { 52 return nil, nil, fmt.Errorf("unable to parse yaml workflow file: %w", errs) 53 } 54 55 // unmarshal the node into a workflowDef struct 56 var wf workflowDef 57 if errs = node.Decode(&wf); errs != nil { 58 return nil, nil, fmt.Errorf("unable to decode workflow: %w", errs) 59 } 60 61 attachUsageComments(&node, &wf) 62 63 // we use a collection to help with deduplication before raising to higher level processing 64 pkgs := pkg.NewCollection() 65 66 for _, job := range wf.Jobs { 67 if job.Uses != "" { 68 p, err := newPackageFromUsageStatement(job.Uses, job.UsesComment, reader.Location) 69 if err != nil { 70 errs = unknown.Append(errs, reader, err) 71 } 72 if p != nil { 73 pkgs.Add(*p) 74 } 75 } 76 } 77 78 return pkgs.Sorted(), nil, errs 79 } 80 81 func parseWorkflowForActionUsage(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { 82 contents, errs := io.ReadAll(reader) 83 if errs != nil { 84 return nil, nil, fmt.Errorf("unable to read yaml workflow file: %w", errs) 85 } 86 87 // parse the yaml file into a generic node to preserve comments 88 var node yaml.Node 89 if errs = yaml.Unmarshal(contents, &node); errs != nil { 90 return nil, nil, fmt.Errorf("unable to parse yaml workflow file: %w", errs) 91 } 92 93 // unmarshal the node into a workflowDef struct 94 var wf workflowDef 95 if errs = node.Decode(&wf); errs != nil { 96 return nil, nil, fmt.Errorf("unable to decode workflow: %w", errs) 97 } 98 99 attachUsageComments(&node, &wf) 100 101 // we use a collection to help with deduplication before raising to higher level processing 102 pkgs := pkg.NewCollection() 103 104 for _, job := range wf.Jobs { 105 for _, step := range job.Steps { 106 if step.Uses == "" { 107 continue 108 } 109 p, err := newPackageFromUsageStatement(step.Uses, step.UsesComment, reader.Location) 110 if err != nil { 111 errs = unknown.Append(errs, reader, err) 112 } 113 if p != nil { 114 pkgs.Add(*p) 115 } 116 } 117 } 118 119 return pkgs.Sorted(), nil, errs 120 } 121 122 // attachUsageComments traverses the yaml node tree and attaches usage comments to the workflowDef job strcuts and step structs. 123 // This is a best-effort approach to attach comments to the correct job or step. 124 func attachUsageComments(node *yaml.Node, wf *workflowDef) { 125 // for a document node, process its content (usually a single mapping node) 126 if node.Kind == yaml.DocumentNode && len(node.Content) > 0 { 127 processNode(node.Content[0], wf, nil, nil, nil) 128 } else { 129 processNode(node, wf, nil, nil, nil) 130 } 131 } 132 133 func processNode(node *yaml.Node, wf *workflowDef, currentJob *string, currentStep *int, inJobsSection *bool) { 134 switch node.Kind { 135 case yaml.MappingNode: 136 for i := 0; i < len(node.Content); i += 2 { 137 key := node.Content[i] 138 value := node.Content[i+1] 139 140 // track if we're in the jobs section... 141 if key.Value == "jobs" && inJobsSection == nil { 142 inJobs := true 143 inJobsSection = &inJobs 144 processNode(value, wf, nil, nil, inJobsSection) 145 continue 146 } 147 148 // if we're in jobs section, and this is a job key... 149 if inJobsSection != nil && *inJobsSection && currentJob == nil { 150 job := key.Value 151 currentJob = &job 152 processNode(value, wf, currentJob, nil, inJobsSection) 153 currentJob = nil 154 continue 155 } 156 157 // if this is a "uses" key... 158 if key.Value == "uses" { 159 processUsesNode(value, wf, currentJob, currentStep) 160 } 161 162 // if this is a "steps" key inside a job... 163 if key.Value == "steps" && currentJob != nil { 164 for j, stepNode := range value.Content { 165 stepIndex := j 166 processNode(stepNode, wf, currentJob, &stepIndex, inJobsSection) 167 } 168 continue 169 } 170 171 processNode(key, wf, currentJob, currentStep, inJobsSection) 172 processNode(value, wf, currentJob, currentStep, inJobsSection) 173 } 174 175 case yaml.SequenceNode: 176 for i, item := range node.Content { 177 idx := i 178 processNode(item, wf, currentJob, &idx, inJobsSection) 179 } 180 } 181 } 182 183 func processUsesNode(node *yaml.Node, wf *workflowDef, currentJob *string, currentStep *int) { 184 if node.Kind != yaml.ScalarNode { 185 return 186 } 187 188 comment := node.LineComment 189 if comment == "" { 190 comment = node.HeadComment 191 } 192 if comment == "" { 193 comment = node.FootComment 194 } 195 196 if comment != "" { 197 versionRegex := regexp.MustCompile(`v?\d+(\.\d+)*`) 198 versionMatch := versionRegex.FindString(comment) 199 200 if versionMatch != "" { 201 if currentJob != nil && currentStep == nil { 202 // this is a job level "uses" 203 if job, ok := wf.Jobs[*currentJob]; ok { 204 job.UsesComment = versionMatch 205 wf.Jobs[*currentJob] = job 206 } 207 } else if currentJob != nil && currentStep != nil { 208 // this is a step level "uses" 209 if job, ok := wf.Jobs[*currentJob]; ok { 210 if *currentStep < len(job.Steps) { 211 job.Steps[*currentStep].UsesComment = versionMatch 212 wf.Jobs[*currentJob] = job 213 } 214 } 215 } 216 } 217 } 218 }