fix: composite action version parsing (#4616)

Signed-off-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
Keith Zantow 2026-06-29 15:23:47 -04:00 committed by GitHub
parent e7f1a803e7
commit deee79411a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 99 additions and 4 deletions

View File

@ -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 {

View File

@ -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
}
}
}
}

View File

@ -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",

View File

@ -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 != "" {

View File

@ -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