mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
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:
parent
f851085668
commit
12f36420dd
@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
2914
schema/json/schema-16.0.25.json
Normal file
2914
schema/json/schema-16.0.25.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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{},
|
||||||
|
|||||||
@ -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"),
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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").
|
||||||
|
|||||||
@ -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
6
syft/pkg/github.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package pkg
|
||||||
|
|
||||||
|
type GitHubActionsUseStatement struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user