diff --git a/syft/pkg/cataloger/githubactions/package.go b/syft/pkg/cataloger/githubactions/package.go index cba6cea89..179957510 100644 --- a/syft/pkg/cataloger/githubactions/package.go +++ b/syft/pkg/cataloger/githubactions/package.go @@ -73,7 +73,6 @@ func parseStepUsageStatement(use, comment string) (string, string) { // if version looks like a commit hash and we have a comment, try to extract version from comment if version != "" && regexp.MustCompile(`^[0-9a-f]{7,}$`).MatchString(version) && comment != "" { - versionRegex := regexp.MustCompile(`v?\d+\.\d+\.\d+`) matches := versionRegex.FindStringSubmatch(comment) if len(matches) >= 1 { diff --git a/syft/pkg/cataloger/githubactions/parse_composite_action.go b/syft/pkg/cataloger/githubactions/parse_composite_action.go index 96378815e..09949aa03 100644 --- a/syft/pkg/cataloger/githubactions/parse_composite_action.go +++ b/syft/pkg/cataloger/githubactions/parse_composite_action.go @@ -24,12 +24,19 @@ type compositeActionRunsDef struct { } func parseCompositeActionForActionUsage(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { - var ca compositeActionDef var errs error - if errs = yaml.NewDecoder(reader).Decode(&ca); errs != nil { + var node yaml.Node + if errs = yaml.NewDecoder(reader).Decode(&node); errs != nil { + return nil, nil, fmt.Errorf("unable to parse yaml workflow file: %w", errs) + } + + var ca compositeActionDef + if errs = node.Decode(&ca); errs != nil { return nil, nil, fmt.Errorf("unable to parse yaml composite action file: %w", errs) } + attachCompositeActionUsageComments(&node, ca.Runs.Steps) + // we use a collection to help with deduplication before raising to higher level processing pkgs := pkg.NewCollection() @@ -49,3 +56,62 @@ func parseCompositeActionForActionUsage(_ context.Context, _ file.Resolver, _ *g return pkgs.Sorted(), nil, errs } + +func attachCompositeActionUsageComments(node *yaml.Node, steps []stepDef) { + root := node + if root.Kind == yaml.DocumentNode && len(root.Content) > 0 { + root = root.Content[0] + } + if root.Kind != yaml.MappingNode { + return + } + + // find the "runs" key + for i := 0; i < len(root.Content); i += 2 { + key := root.Content[i] + value := root.Content[i+1] + if key.Value != "runs" || value.Kind != yaml.MappingNode { + continue + } + // find the "steps" key within runs + for j := 0; j < len(value.Content); j += 2 { + stepsKey := value.Content[j] + stepsValue := value.Content[j+1] + if stepsKey.Value != "steps" || stepsValue.Kind != yaml.SequenceNode { + continue + } + readSteps(stepsValue, steps) + } + } +} + +func readSteps(stepsValue *yaml.Node, steps []stepDef) { + // iterate over each step + for stepIdx, stepNode := range stepsValue.Content { + if stepNode.Kind != yaml.MappingNode { + continue + } + // find the "uses" key within the step + for k := 0; k < len(stepNode.Content); k += 2 { + usesKey := stepNode.Content[k] + usesValue := stepNode.Content[k+1] + if usesKey.Value != "uses" || usesValue.Kind != yaml.ScalarNode { + continue + } + comment := usesValue.LineComment + if comment == "" { + comment = usesValue.HeadComment + } + if comment == "" { + comment = usesValue.FootComment + } + if comment == "" { + continue + } + versionMatch := versionRegex.FindString(comment) + if versionMatch != "" && stepIdx < len(steps) { + steps[stepIdx].UsesComment = versionMatch + } + } + } +} diff --git a/syft/pkg/cataloger/githubactions/parse_composite_action_test.go b/syft/pkg/cataloger/githubactions/parse_composite_action_test.go index 2ed607fe3..b089f939f 100644 --- a/syft/pkg/cataloger/githubactions/parse_composite_action_test.go +++ b/syft/pkg/cataloger/githubactions/parse_composite_action_test.go @@ -14,6 +14,28 @@ func Test_parseCompositeActionForActionUsage(t *testing.T) { fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) expected := []pkg.Package{ + { + Name: "actions/checkout", + Version: "11", + Type: pkg.GithubActionPkg, + Locations: fixtureLocationSet, + PURL: "pkg:github/actions/checkout@11", + Metadata: pkg.GitHubActionsUseStatement{ + Value: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683", + Comment: "11", + }, + }, + { + Name: "actions/setup-go", + Version: "v5.1.0", + Type: pkg.GithubActionPkg, + Locations: fixtureLocationSet, + PURL: "pkg:github/actions/setup-go@v5.1.0", + Metadata: pkg.GitHubActionsUseStatement{ + Value: "actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed", + Comment: "v5.1.0", + }, + }, { Name: "actions/setup-go", Version: "v4", diff --git a/syft/pkg/cataloger/githubactions/parse_workflow.go b/syft/pkg/cataloger/githubactions/parse_workflow.go index f784bce63..1957ac2f5 100644 --- a/syft/pkg/cataloger/githubactions/parse_workflow.go +++ b/syft/pkg/cataloger/githubactions/parse_workflow.go @@ -19,6 +19,8 @@ var ( _ generic.Parser = parseWorkflowForWorkflowUsage ) +var versionRegex = regexp.MustCompile(`v?\d+(\.\d+)*`) + type workflowDef struct { Jobs map[string]workflowJobDef `yaml:"jobs"` } @@ -185,7 +187,6 @@ func processUsesNode(node *yaml.Node, wf *workflowDef, currentJob *string, curre } if comment != "" { - versionRegex := regexp.MustCompile(`v?\d+(\.\d+)*`) versionMatch := versionRegex.FindString(comment) if versionMatch != "" { diff --git a/syft/pkg/cataloger/githubactions/testdata/composite-action.yaml b/syft/pkg/cataloger/githubactions/testdata/composite-action.yaml index b294c90ae..8e8038b12 100644 --- a/syft/pkg/cataloger/githubactions/testdata/composite-action.yaml +++ b/syft/pkg/cataloger/githubactions/testdata/composite-action.yaml @@ -35,6 +35,13 @@ runs: path: ${{ github.workspace }}/.tmp key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('Makefile') }} + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v 11 + + - name: Setup Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed #v5.1.0 + with: + go-version: ${{ inputs.go-version }} + # note: we need to keep restoring the go mod cache before bootstrapping tools since `go install` is used in # some installations of project tools. - name: Restore go module cache