mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 14:02:26 +01:00
219 lines
5.9 KiB
Go
219 lines
5.9 KiB
Go
package githubactions
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
|
|
"go.yaml.in/yaml/v3"
|
|
|
|
"github.com/anchore/syft/internal/unknown"
|
|
"github.com/anchore/syft/syft/artifact"
|
|
"github.com/anchore/syft/syft/file"
|
|
"github.com/anchore/syft/syft/pkg"
|
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
|
)
|
|
|
|
var (
|
|
_ generic.Parser = parseWorkflowForActionUsage
|
|
_ generic.Parser = parseWorkflowForWorkflowUsage
|
|
)
|
|
|
|
type workflowDef struct {
|
|
Jobs map[string]workflowJobDef `yaml:"jobs"`
|
|
}
|
|
|
|
type workflowJobDef struct {
|
|
Uses string `yaml:"uses"`
|
|
UsesComment string `yaml:"-"`
|
|
Steps []stepDef `yaml:"steps"`
|
|
}
|
|
|
|
type stepDef struct {
|
|
Name string `yaml:"name"`
|
|
Uses string `yaml:"uses"`
|
|
UsesComment string `yaml:"-"`
|
|
With struct {
|
|
Path string `yaml:"path"`
|
|
Key string `yaml:"key"`
|
|
} `yaml:"with"`
|
|
}
|
|
|
|
func parseWorkflowForWorkflowUsage(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
|
contents, errs := io.ReadAll(reader)
|
|
if errs != nil {
|
|
return nil, nil, fmt.Errorf("unable to read yaml workflow file: %w", errs)
|
|
}
|
|
|
|
// parse the yaml file into a generic node to preserve comments
|
|
var node yaml.Node
|
|
if errs = yaml.Unmarshal(contents, &node); errs != nil {
|
|
return nil, nil, fmt.Errorf("unable to parse yaml workflow file: %w", errs)
|
|
}
|
|
|
|
// unmarshal the node into a workflowDef struct
|
|
var wf workflowDef
|
|
if errs = node.Decode(&wf); errs != nil {
|
|
return nil, nil, fmt.Errorf("unable to decode workflow: %w", errs)
|
|
}
|
|
|
|
attachUsageComments(&node, &wf)
|
|
|
|
// we use a collection to help with deduplication before raising to higher level processing
|
|
pkgs := pkg.NewCollection()
|
|
|
|
for _, job := range wf.Jobs {
|
|
if job.Uses != "" {
|
|
p, err := newPackageFromUsageStatement(job.Uses, job.UsesComment, reader.Location)
|
|
if err != nil {
|
|
errs = unknown.Append(errs, reader, err)
|
|
}
|
|
if p != nil {
|
|
pkgs.Add(*p)
|
|
}
|
|
}
|
|
}
|
|
|
|
return pkgs.Sorted(), nil, errs
|
|
}
|
|
|
|
func parseWorkflowForActionUsage(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
|
contents, errs := io.ReadAll(reader)
|
|
if errs != nil {
|
|
return nil, nil, fmt.Errorf("unable to read yaml workflow file: %w", errs)
|
|
}
|
|
|
|
// parse the yaml file into a generic node to preserve comments
|
|
var node yaml.Node
|
|
if errs = yaml.Unmarshal(contents, &node); errs != nil {
|
|
return nil, nil, fmt.Errorf("unable to parse yaml workflow file: %w", errs)
|
|
}
|
|
|
|
// unmarshal the node into a workflowDef struct
|
|
var wf workflowDef
|
|
if errs = node.Decode(&wf); errs != nil {
|
|
return nil, nil, fmt.Errorf("unable to decode workflow: %w", errs)
|
|
}
|
|
|
|
attachUsageComments(&node, &wf)
|
|
|
|
// we use a collection to help with deduplication before raising to higher level processing
|
|
pkgs := pkg.NewCollection()
|
|
|
|
for _, job := range wf.Jobs {
|
|
for _, step := range job.Steps {
|
|
if step.Uses == "" {
|
|
continue
|
|
}
|
|
p, err := newPackageFromUsageStatement(step.Uses, step.UsesComment, reader.Location)
|
|
if err != nil {
|
|
errs = unknown.Append(errs, reader, err)
|
|
}
|
|
if p != nil {
|
|
pkgs.Add(*p)
|
|
}
|
|
}
|
|
}
|
|
|
|
return pkgs.Sorted(), nil, errs
|
|
}
|
|
|
|
// attachUsageComments traverses the yaml node tree and attaches usage comments to the workflowDef job strcuts and step structs.
|
|
// This is a best-effort approach to attach comments to the correct job or step.
|
|
func attachUsageComments(node *yaml.Node, wf *workflowDef) {
|
|
// for a document node, process its content (usually a single mapping node)
|
|
if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
|
|
processNode(node.Content[0], wf, nil, nil, nil)
|
|
} else {
|
|
processNode(node, wf, nil, nil, nil)
|
|
}
|
|
}
|
|
|
|
func processNode(node *yaml.Node, wf *workflowDef, currentJob *string, currentStep *int, inJobsSection *bool) {
|
|
switch node.Kind {
|
|
case yaml.MappingNode:
|
|
for i := 0; i < len(node.Content); i += 2 {
|
|
key := node.Content[i]
|
|
value := node.Content[i+1]
|
|
|
|
// track if we're in the jobs section...
|
|
if key.Value == "jobs" && inJobsSection == nil {
|
|
inJobs := true
|
|
inJobsSection = &inJobs
|
|
processNode(value, wf, nil, nil, inJobsSection)
|
|
continue
|
|
}
|
|
|
|
// if we're in jobs section, and this is a job key...
|
|
if inJobsSection != nil && *inJobsSection && currentJob == nil {
|
|
job := key.Value
|
|
currentJob = &job
|
|
processNode(value, wf, currentJob, nil, inJobsSection)
|
|
currentJob = nil
|
|
continue
|
|
}
|
|
|
|
// if this is a "uses" key...
|
|
if key.Value == "uses" {
|
|
processUsesNode(value, wf, currentJob, currentStep)
|
|
}
|
|
|
|
// if this is a "steps" key inside a job...
|
|
if key.Value == "steps" && currentJob != nil {
|
|
for j, stepNode := range value.Content {
|
|
stepIndex := j
|
|
processNode(stepNode, wf, currentJob, &stepIndex, inJobsSection)
|
|
}
|
|
continue
|
|
}
|
|
|
|
processNode(key, wf, currentJob, currentStep, inJobsSection)
|
|
processNode(value, wf, currentJob, currentStep, inJobsSection)
|
|
}
|
|
|
|
case yaml.SequenceNode:
|
|
for i, item := range node.Content {
|
|
idx := i
|
|
processNode(item, wf, currentJob, &idx, inJobsSection)
|
|
}
|
|
}
|
|
}
|
|
|
|
func processUsesNode(node *yaml.Node, wf *workflowDef, currentJob *string, currentStep *int) {
|
|
if node.Kind != yaml.ScalarNode {
|
|
return
|
|
}
|
|
|
|
comment := node.LineComment
|
|
if comment == "" {
|
|
comment = node.HeadComment
|
|
}
|
|
if comment == "" {
|
|
comment = node.FootComment
|
|
}
|
|
|
|
if comment != "" {
|
|
versionRegex := regexp.MustCompile(`v?\d+(\.\d+)*`)
|
|
versionMatch := versionRegex.FindString(comment)
|
|
|
|
if versionMatch != "" {
|
|
if currentJob != nil && currentStep == nil {
|
|
// this is a job level "uses"
|
|
if job, ok := wf.Jobs[*currentJob]; ok {
|
|
job.UsesComment = versionMatch
|
|
wf.Jobs[*currentJob] = job
|
|
}
|
|
} else if currentJob != nil && currentStep != nil {
|
|
// this is a step level "uses"
|
|
if job, ok := wf.Jobs[*currentJob]; ok {
|
|
if *currentStep < len(job.Steps) {
|
|
job.Steps[*currentStep].UsesComment = versionMatch
|
|
wf.Jobs[*currentJob] = job
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|