github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/githubactions/package.go (about)

     1  package githubactions
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strings"
     7  
     8  	"github.com/anchore/packageurl-go"
     9  	"github.com/anchore/syft/internal/log"
    10  	"github.com/anchore/syft/syft/file"
    11  	"github.com/anchore/syft/syft/pkg"
    12  )
    13  
    14  func newPackageFromUsageStatement(use, comment string, location file.Location) (*pkg.Package, error) {
    15  	name, version := parseStepUsageStatement(use, comment)
    16  
    17  	if name == "" {
    18  		log.WithFields("file", location.RealPath, "statement", use).Trace("unable to parse github action usage statement")
    19  		return nil, fmt.Errorf("unable to parse github action usage statement")
    20  	}
    21  
    22  	if strings.Contains(name, ".github/workflows/") {
    23  		return newGithubActionWorkflowPackageUsage(name, version, location, pkg.GitHubActionsUseStatement{Value: use, Comment: comment}), nil
    24  	}
    25  
    26  	return newGithubActionPackageUsage(name, version, location, pkg.GitHubActionsUseStatement{Value: use, Comment: comment}), nil
    27  }
    28  
    29  func newGithubActionWorkflowPackageUsage(name, version string, workflowLocation file.Location, m pkg.GitHubActionsUseStatement) *pkg.Package {
    30  	p := &pkg.Package{
    31  		Name:      name,
    32  		Version:   version,
    33  		Locations: file.NewLocationSet(workflowLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
    34  		PURL:      packageURL(name, version),
    35  		Type:      pkg.GithubActionWorkflowPkg,
    36  		Metadata:  m,
    37  	}
    38  
    39  	p.SetID()
    40  
    41  	return p
    42  }
    43  
    44  func newGithubActionPackageUsage(name, version string, workflowLocation file.Location, m pkg.GitHubActionsUseStatement) *pkg.Package {
    45  	p := &pkg.Package{
    46  		Name:      name,
    47  		Version:   version,
    48  		Locations: file.NewLocationSet(workflowLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
    49  		PURL:      packageURL(name, version),
    50  		Type:      pkg.GithubActionPkg,
    51  		Metadata:  m,
    52  	}
    53  
    54  	p.SetID()
    55  
    56  	return p
    57  }
    58  
    59  func parseStepUsageStatement(use, comment string) (string, string) {
    60  	// from "octo-org/another-repo/.github/workflows/workflow.yml@v1" get octo-org/another-repo/.github/workflows/workflow.yml and v1
    61  	// from "./.github/workflows/workflow-2.yml" interpret as only the name
    62  	// from "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2" get actions/checkout and v4.2.2
    63  	// from "actions/cache@v3" get actions/cache and v3
    64  
    65  	fields := strings.Split(use, "@")
    66  	name := use
    67  	version := ""
    68  
    69  	if len(fields) == 2 {
    70  		name = fields[0]
    71  		version = fields[1]
    72  	}
    73  
    74  	// if version looks like a commit hash and we have a comment, try to extract version from comment
    75  	if version != "" && regexp.MustCompile(`^[0-9a-f]{7,}$`).MatchString(version) && comment != "" {
    76  		versionRegex := regexp.MustCompile(`v?\d+\.\d+\.\d+`)
    77  		matches := versionRegex.FindStringSubmatch(comment)
    78  
    79  		if len(matches) >= 1 {
    80  			return name, matches[0]
    81  		}
    82  	}
    83  
    84  	return name, version
    85  }
    86  
    87  func packageURL(name, version string) string {
    88  	var qualifiers packageurl.Qualifiers
    89  	var subPath string
    90  	var namespace string
    91  
    92  	fields := strings.SplitN(name, "/", 3)
    93  	switch len(fields) {
    94  	case 1:
    95  		return ""
    96  	case 2:
    97  		namespace = fields[0]
    98  		name = fields[1]
    99  	case 3:
   100  		namespace = fields[0]
   101  		name = fields[1]
   102  		subPath = fields[2]
   103  	}
   104  	if namespace == "." {
   105  		// this is a local composite action, which is unclear how to represent in a PURL without more information
   106  		return ""
   107  	}
   108  
   109  	// there isn't a github actions PURL but there is a github PURL type for referencing github repos, which is the
   110  	// next best thing until there is a supported type.
   111  	return packageurl.NewPackageURL(
   112  		packageurl.TypeGithub,
   113  		namespace,
   114  		name,
   115  		version,
   116  		qualifiers,
   117  		subPath,
   118  	).ToString()
   119  }