Parse GitHub actions comments (#3776)

* add version comment parsing support to github actions

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update json schema with github actions metadata

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add originator processing for github actions type

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2025-04-03 10:46:27 -04:00 committed by GitHub
parent f851085668
commit 12f36420dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 3199 additions and 40 deletions

View File

@ -3,5 +3,5 @@ package internal
const ( const (
// JSONSchemaVersion is the current schema version output by the JSON encoder // JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment. // This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "16.0.24" JSONSchemaVersion = "16.0.25"
) )

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.0.24/document", "$id": "anchore.io/schema/syft/json/16.0.25/document",
"$ref": "#/$defs/Document", "$ref": "#/$defs/Document",
"$defs": { "$defs": {
"AlpmDbEntry": { "AlpmDbEntry": {
@ -995,6 +995,20 @@
"size" "size"
] ]
}, },
"GithubActionsUseStatement": {
"properties": {
"value": {
"type": "string"
},
"comment": {
"type": "string"
}
},
"type": "object",
"required": [
"value"
]
},
"GoModuleBuildinfoEntry": { "GoModuleBuildinfoEntry": {
"properties": { "properties": {
"goBuildSettings": { "goBuildSettings": {
@ -1804,6 +1818,9 @@
{ {
"$ref": "#/$defs/ErlangRebarLockEntry" "$ref": "#/$defs/ErlangRebarLockEntry"
}, },
{
"$ref": "#/$defs/GithubActionsUseStatement"
},
{ {
"$ref": "#/$defs/GoModuleBuildinfoEntry" "$ref": "#/$defs/GoModuleBuildinfoEntry"
}, },

View File

@ -34,7 +34,7 @@ const (
// //
// Available options are: <omit>, NOASSERTION, Person: <person>, Organization: <org> // Available options are: <omit>, NOASSERTION, Person: <person>, Organization: <org>
// return values are: <type>, <value> // return values are: <type>, <value>
func Originator(p pkg.Package) (typ string, author string) { //nolint: funlen func Originator(p pkg.Package) (typ string, author string) { //nolint: gocyclo,funlen
if !hasMetadata(p) { if !hasMetadata(p) {
return typ, author return typ, author
} }
@ -57,6 +57,15 @@ func Originator(p pkg.Package) (typ string, author string) { //nolint: funlen
case pkg.DpkgArchiveEntry: case pkg.DpkgArchiveEntry:
author = metadata.Maintainer author = metadata.Maintainer
case pkg.GitHubActionsUseStatement:
typ = orgType
org := strings.Split(metadata.Value, "/")[0]
if org == "actions" {
// this is a GitHub action, so the org is GitHub
org = "GitHub"
}
author = org
case pkg.JavaArchive: case pkg.JavaArchive:
if metadata.Manifest != nil { if metadata.Manifest != nil {
author = metadata.Manifest.Main.MustGet("Specification-Vendor") author = metadata.Manifest.Main.MustGet("Specification-Vendor")

View File

@ -45,6 +45,7 @@ func Test_OriginatorSupplier(t *testing.T) {
pkg.SwiplPackEntry{}, pkg.SwiplPackEntry{},
pkg.OpamPackage{}, pkg.OpamPackage{},
pkg.YarnLockEntry{}, pkg.YarnLockEntry{},
pkg.TerraformLockProviderEntry{},
) )
tests := []struct { tests := []struct {
name string name string
@ -384,20 +385,24 @@ func Test_OriginatorSupplier(t *testing.T) {
supplier: "Person: me (me@auth.com)", supplier: "Person: me (me@auth.com)",
}, },
{ {
name: "from ocaml opam", name: "from github actions workflow/action",
input: pkg.Package{ input: pkg.Package{
Metadata: pkg.OpamPackage{}, Metadata: pkg.GitHubActionsUseStatement{
Value: "actions/checkout@v4",
},
}, },
originator: "", originator: "Organization: GitHub",
supplier: "", supplier: "Organization: GitHub",
}, },
{ {
name: "from terraform lock", name: "from github actions workflow/action",
input: pkg.Package{ input: pkg.Package{
Metadata: pkg.TerraformLockProviderEntry{}, Metadata: pkg.GitHubActionsUseStatement{
Value: "google/something@v6",
},
}, },
originator: "", originator: "Organization: google",
supplier: "", supplier: "Organization: google",
}, },
} }
for _, test := range tests { for _, test := range tests {

View File

@ -25,6 +25,7 @@ func AllTypes() []any {
pkg.ELFBinaryPackageNoteJSONPayload{}, pkg.ELFBinaryPackageNoteJSONPayload{},
pkg.ElixirMixLockEntry{}, pkg.ElixirMixLockEntry{},
pkg.ErlangRebarLockEntry{}, pkg.ErlangRebarLockEntry{},
pkg.GitHubActionsUseStatement{},
pkg.GolangBinaryBuildinfoEntry{}, pkg.GolangBinaryBuildinfoEntry{},
pkg.GolangModuleEntry{}, pkg.GolangModuleEntry{},
pkg.HackageStackYamlEntry{}, pkg.HackageStackYamlEntry{},

View File

@ -78,6 +78,7 @@ var jsonTypes = makeJSONTypes(
jsonNames(pkg.DpkgDBEntry{}, "dpkg-db-entry", "DpkgMetadata"), jsonNames(pkg.DpkgDBEntry{}, "dpkg-db-entry", "DpkgMetadata"),
jsonNames(pkg.ELFBinaryPackageNoteJSONPayload{}, "elf-binary-package-note-json-payload"), jsonNames(pkg.ELFBinaryPackageNoteJSONPayload{}, "elf-binary-package-note-json-payload"),
jsonNames(pkg.RubyGemspec{}, "ruby-gemspec", "GemMetadata"), jsonNames(pkg.RubyGemspec{}, "ruby-gemspec", "GemMetadata"),
jsonNames(pkg.GitHubActionsUseStatement{}, "github-actions-use-statement"),
jsonNames(pkg.GolangBinaryBuildinfoEntry{}, "go-module-buildinfo-entry", "GolangBinMetadata", "GolangMetadata"), jsonNames(pkg.GolangBinaryBuildinfoEntry{}, "go-module-buildinfo-entry", "GolangBinMetadata", "GolangMetadata"),
jsonNames(pkg.GolangModuleEntry{}, "go-module-entry", "GolangModMetadata"), jsonNames(pkg.GolangModuleEntry{}, "go-module-entry", "GolangModMetadata"),
jsonNames(pkg.HackageStackYamlLockEntry{}, "haskell-hackage-stack-lock-entry", "HackageMetadataType"), jsonNames(pkg.HackageStackYamlLockEntry{}, "haskell-hackage-stack-lock-entry", "HackageMetadataType"),

View File

@ -2,6 +2,7 @@ package githubactions
import ( import (
"fmt" "fmt"
"regexp"
"strings" "strings"
"github.com/anchore/packageurl-go" "github.com/anchore/packageurl-go"
@ -10,8 +11,8 @@ import (
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
func newPackageFromUsageStatement(use string, location file.Location) (*pkg.Package, error) { func newPackageFromUsageStatement(use, comment string, location file.Location) (*pkg.Package, error) {
name, version := parseStepUsageStatement(use) name, version := parseStepUsageStatement(use, comment)
if name == "" { if name == "" {
log.WithFields("file", location.RealPath, "statement", use).Trace("unable to parse github action usage statement") log.WithFields("file", location.RealPath, "statement", use).Trace("unable to parse github action usage statement")
@ -19,19 +20,20 @@ func newPackageFromUsageStatement(use string, location file.Location) (*pkg.Pack
} }
if strings.Contains(name, ".github/workflows/") { if strings.Contains(name, ".github/workflows/") {
return newGithubActionWorkflowPackageUsage(name, version, location), nil return newGithubActionWorkflowPackageUsage(name, version, location, pkg.GitHubActionsUseStatement{Value: use, Comment: comment}), nil
} }
return newGithubActionPackageUsage(name, version, location), nil return newGithubActionPackageUsage(name, version, location, pkg.GitHubActionsUseStatement{Value: use, Comment: comment}), nil
} }
func newGithubActionWorkflowPackageUsage(name, version string, workflowLocation file.Location) *pkg.Package { func newGithubActionWorkflowPackageUsage(name, version string, workflowLocation file.Location, m pkg.GitHubActionsUseStatement) *pkg.Package {
p := &pkg.Package{ p := &pkg.Package{
Name: name, Name: name,
Version: version, Version: version,
Locations: file.NewLocationSet(workflowLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), Locations: file.NewLocationSet(workflowLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
PURL: packageURL(name, version), PURL: packageURL(name, version),
Type: pkg.GithubActionWorkflowPkg, Type: pkg.GithubActionWorkflowPkg,
Metadata: m,
} }
p.SetID() p.SetID()
@ -39,13 +41,14 @@ func newGithubActionWorkflowPackageUsage(name, version string, workflowLocation
return p return p
} }
func newGithubActionPackageUsage(name, version string, workflowLocation file.Location) *pkg.Package { func newGithubActionPackageUsage(name, version string, workflowLocation file.Location, m pkg.GitHubActionsUseStatement) *pkg.Package {
p := &pkg.Package{ p := &pkg.Package{
Name: name, Name: name,
Version: version, Version: version,
Locations: file.NewLocationSet(workflowLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), Locations: file.NewLocationSet(workflowLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
PURL: packageURL(name, version), PURL: packageURL(name, version),
Type: pkg.GithubActionPkg, Type: pkg.GithubActionPkg,
Metadata: m,
} }
p.SetID() p.SetID()
@ -53,20 +56,32 @@ func newGithubActionPackageUsage(name, version string, workflowLocation file.Loc
return p return p
} }
func parseStepUsageStatement(use string) (string, string) { func parseStepUsageStatement(use, comment string) (string, string) {
// from octo-org/another-repo/.github/workflows/workflow.yml@v1 get octo-org/another-repo/.github/workflows/workflow.yml and v1 // from "octo-org/another-repo/.github/workflows/workflow.yml@v1" get octo-org/another-repo/.github/workflows/workflow.yml and v1
// from ./.github/workflows/workflow-2.yml interpret as only the name // from "./.github/workflows/workflow-2.yml" interpret as only the name
// from "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2" get actions/checkout and v4.2.2
// from actions/cache@v3 get actions/cache and v3 // from "actions/cache@v3" get actions/cache and v3
fields := strings.Split(use, "@") fields := strings.Split(use, "@")
switch len(fields) { name := use
case 1: version := ""
return use, ""
case 2: if len(fields) == 2 {
return fields[0], fields[1] name = fields[0]
version = fields[1]
} }
return "", ""
// 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 {
return name, matches[0]
}
}
return name, version
} }
func packageURL(name, version string) string { func packageURL(name, version string) string {

View File

@ -43,7 +43,7 @@ func parseCompositeActionForActionUsage(_ context.Context, _ file.Resolver, _ *g
continue continue
} }
p, err := newPackageFromUsageStatement(step.Uses, reader.Location) p, err := newPackageFromUsageStatement(step.Uses, step.UsesComment, reader.Location)
if err != nil { if err != nil {
errs = unknown.Append(errs, reader, err) errs = unknown.Append(errs, reader, err)
} }

View File

@ -20,6 +20,7 @@ func Test_parseCompositeActionForActionUsage(t *testing.T) {
Type: pkg.GithubActionPkg, Type: pkg.GithubActionPkg,
Locations: fixtureLocationSet, Locations: fixtureLocationSet,
PURL: "pkg:github/actions/setup-go@v4", PURL: "pkg:github/actions/setup-go@v4",
Metadata: pkg.GitHubActionsUseStatement{Value: "actions/setup-go@v4"},
}, },
{ {
Name: "actions/cache", Name: "actions/cache",
@ -27,6 +28,7 @@ func Test_parseCompositeActionForActionUsage(t *testing.T) {
Type: pkg.GithubActionPkg, Type: pkg.GithubActionPkg,
Locations: fixtureLocationSet, Locations: fixtureLocationSet,
PURL: "pkg:github/actions/cache@v3", PURL: "pkg:github/actions/cache@v3",
Metadata: pkg.GitHubActionsUseStatement{Value: "actions/cache@v3"},
}, },
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"regexp"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -24,14 +25,16 @@ type workflowDef struct {
} }
type workflowJobDef struct { type workflowJobDef struct {
Uses string `yaml:"uses"` Uses string `yaml:"uses"`
Steps []stepDef `yaml:"steps"` UsesComment string `yaml:"-"`
Steps []stepDef `yaml:"steps"`
} }
type stepDef struct { type stepDef struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Uses string `yaml:"uses"` Uses string `yaml:"uses"`
With struct { UsesComment string `yaml:"-"`
With struct {
Path string `yaml:"path"` Path string `yaml:"path"`
Key string `yaml:"key"` Key string `yaml:"key"`
} `yaml:"with"` } `yaml:"with"`
@ -43,17 +46,26 @@ func parseWorkflowForWorkflowUsage(_ context.Context, _ file.Resolver, _ *generi
return nil, nil, fmt.Errorf("unable to read yaml workflow file: %w", errs) return nil, nil, fmt.Errorf("unable to read yaml workflow file: %w", errs)
} }
var wf workflowDef // parse the yaml file into a generic node to preserve comments
if errs = yaml.Unmarshal(contents, &wf); errs != nil { 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) 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 // we use a collection to help with deduplication before raising to higher level processing
pkgs := pkg.NewCollection() pkgs := pkg.NewCollection()
for _, job := range wf.Jobs { for _, job := range wf.Jobs {
if job.Uses != "" { if job.Uses != "" {
p, err := newPackageFromUsageStatement(job.Uses, reader.Location) p, err := newPackageFromUsageStatement(job.Uses, job.UsesComment, reader.Location)
if err != nil { if err != nil {
errs = unknown.Append(errs, reader, err) errs = unknown.Append(errs, reader, err)
} }
@ -72,11 +84,20 @@ func parseWorkflowForActionUsage(_ context.Context, _ file.Resolver, _ *generic.
return nil, nil, fmt.Errorf("unable to read yaml workflow file: %w", errs) return nil, nil, fmt.Errorf("unable to read yaml workflow file: %w", errs)
} }
var wf workflowDef // parse the yaml file into a generic node to preserve comments
if errs = yaml.Unmarshal(contents, &wf); errs != nil { 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) 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 // we use a collection to help with deduplication before raising to higher level processing
pkgs := pkg.NewCollection() pkgs := pkg.NewCollection()
@ -85,7 +106,7 @@ func parseWorkflowForActionUsage(_ context.Context, _ file.Resolver, _ *generic.
if step.Uses == "" { if step.Uses == "" {
continue continue
} }
p, err := newPackageFromUsageStatement(step.Uses, reader.Location) p, err := newPackageFromUsageStatement(step.Uses, step.UsesComment, reader.Location)
if err != nil { if err != nil {
errs = unknown.Append(errs, reader, err) errs = unknown.Append(errs, reader, err)
} }
@ -97,3 +118,101 @@ func parseWorkflowForActionUsage(_ context.Context, _ file.Resolver, _ *generic.
return pkgs.Sorted(), nil, errs 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
}
}
}
}
}
}

View File

@ -20,6 +20,7 @@ func Test_parseWorkflowForActionUsage(t *testing.T) {
Type: pkg.GithubActionPkg, Type: pkg.GithubActionPkg,
Locations: fixtureLocationSet, Locations: fixtureLocationSet,
PURL: "", // don't have enough context without parsing the git origin, which still may not be accurate PURL: "", // don't have enough context without parsing the git origin, which still may not be accurate
Metadata: pkg.GitHubActionsUseStatement{Value: "./.github/actions/bootstrap"},
}, },
{ {
Name: "actions/cache", Name: "actions/cache",
@ -27,6 +28,7 @@ func Test_parseWorkflowForActionUsage(t *testing.T) {
Type: pkg.GithubActionPkg, Type: pkg.GithubActionPkg,
Locations: fixtureLocationSet, Locations: fixtureLocationSet,
PURL: "pkg:github/actions/cache@v3", PURL: "pkg:github/actions/cache@v3",
Metadata: pkg.GitHubActionsUseStatement{Value: "actions/cache@v3"},
}, },
{ {
Name: "actions/cache/restore", Name: "actions/cache/restore",
@ -34,6 +36,7 @@ func Test_parseWorkflowForActionUsage(t *testing.T) {
Type: pkg.GithubActionPkg, Type: pkg.GithubActionPkg,
Locations: fixtureLocationSet, Locations: fixtureLocationSet,
PURL: "pkg:github/actions/cache@v3#restore", PURL: "pkg:github/actions/cache@v3#restore",
Metadata: pkg.GitHubActionsUseStatement{Value: "actions/cache/restore@v3"},
}, },
{ {
Name: "actions/cache/save", Name: "actions/cache/save",
@ -41,6 +44,7 @@ func Test_parseWorkflowForActionUsage(t *testing.T) {
Type: pkg.GithubActionPkg, Type: pkg.GithubActionPkg,
Locations: fixtureLocationSet, Locations: fixtureLocationSet,
PURL: "pkg:github/actions/cache@v3#save", PURL: "pkg:github/actions/cache@v3#save",
Metadata: pkg.GitHubActionsUseStatement{Value: "actions/cache/save@v3"},
}, },
{ {
Name: "actions/checkout", Name: "actions/checkout",
@ -48,6 +52,7 @@ func Test_parseWorkflowForActionUsage(t *testing.T) {
Type: pkg.GithubActionPkg, Type: pkg.GithubActionPkg,
Locations: fixtureLocationSet, Locations: fixtureLocationSet,
PURL: "pkg:github/actions/checkout@v4", PURL: "pkg:github/actions/checkout@v4",
Metadata: pkg.GitHubActionsUseStatement{Value: "actions/checkout@v4"},
}, },
} }
@ -66,6 +71,9 @@ func Test_parseWorkflowForWorkflowUsage(t *testing.T) {
Type: pkg.GithubActionWorkflowPkg, Type: pkg.GithubActionWorkflowPkg,
Locations: fixtureLocationSet, Locations: fixtureLocationSet,
PURL: "pkg:github/octo-org/this-repo@172239021f7ba04fe7327647b213799853a9eb89#.github/workflows/workflow-1.yml", PURL: "pkg:github/octo-org/this-repo@172239021f7ba04fe7327647b213799853a9eb89#.github/workflows/workflow-1.yml",
Metadata: pkg.GitHubActionsUseStatement{
Value: "octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89",
},
}, },
{ {
Name: "./.github/workflows/workflow-2.yml", Name: "./.github/workflows/workflow-2.yml",
@ -73,6 +81,7 @@ func Test_parseWorkflowForWorkflowUsage(t *testing.T) {
Type: pkg.GithubActionWorkflowPkg, Type: pkg.GithubActionWorkflowPkg,
Locations: fixtureLocationSet, Locations: fixtureLocationSet,
PURL: "", // don't have enough context without parsing the git origin, which still may not be accurate PURL: "", // don't have enough context without parsing the git origin, which still may not be accurate
Metadata: pkg.GitHubActionsUseStatement{Value: "./.github/workflows/workflow-2.yml"},
}, },
{ {
Name: "octo-org/another-repo/.github/workflows/workflow.yml", Name: "octo-org/another-repo/.github/workflows/workflow.yml",
@ -80,6 +89,7 @@ func Test_parseWorkflowForWorkflowUsage(t *testing.T) {
Type: pkg.GithubActionWorkflowPkg, Type: pkg.GithubActionWorkflowPkg,
Locations: fixtureLocationSet, Locations: fixtureLocationSet,
PURL: "pkg:github/octo-org/another-repo@v1#.github/workflows/workflow.yml", PURL: "pkg:github/octo-org/another-repo@v1#.github/workflows/workflow.yml",
Metadata: pkg.GitHubActionsUseStatement{Value: "octo-org/another-repo/.github/workflows/workflow.yml@v1"},
}, },
} }
@ -87,6 +97,38 @@ func Test_parseWorkflowForWorkflowUsage(t *testing.T) {
pkgtest.TestFileParser(t, fixture, parseWorkflowForWorkflowUsage, expected, expectedRelationships) pkgtest.TestFileParser(t, fixture, parseWorkflowForWorkflowUsage, expected, expectedRelationships)
} }
func Test_parseWorkflowForVersionComments(t *testing.T) {
fixture := "test-fixtures/workflow-with-version-comments.yaml"
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
expected := []pkg.Package{
{
Name: "./.github/actions/bootstrap",
Version: "",
Type: pkg.GithubActionPkg,
Locations: fixtureLocationSet,
PURL: "", // don't have enough context without parsing the git origin, which still may not be accurate
Metadata: pkg.GitHubActionsUseStatement{
Value: "./.github/actions/bootstrap",
},
},
{
Name: "actions/checkout",
Version: "v4.2.2",
Type: pkg.GithubActionPkg,
Locations: fixtureLocationSet,
PURL: "pkg:github/actions/checkout@v4.2.2",
Metadata: pkg.GitHubActionsUseStatement{
Value: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683",
Comment: "v4.2.2",
},
},
}
var expectedRelationships []artifact.Relationship
pkgtest.TestFileParser(t, fixture, parseWorkflowForActionUsage, expected, expectedRelationships)
}
func Test_corruptActionWorkflow(t *testing.T) { func Test_corruptActionWorkflow(t *testing.T) {
pkgtest.NewCatalogTester(). pkgtest.NewCatalogTester().
FromFile(t, "test-fixtures/corrupt/workflow-multi-job.yaml"). FromFile(t, "test-fixtures/corrupt/workflow-multi-job.yaml").

View File

@ -0,0 +1,28 @@
name: "Validations"
on:
workflow_dispatch:
pull_request:
push:
branches:
- main
permissions:
contents: read
jobs:
call-workflow-1-in-local-repo:
uses: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89 #v1.0.0
Static-Analysis:
name: "Static analysis"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- name: Run static analysis
run: make static-analysis

6
syft/pkg/github.go Normal file
View File

@ -0,0 +1,6 @@
package pkg
type GitHubActionsUseStatement struct {
Value string `json:"value"`
Comment string `json:"comment,omitempty"`
}