WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion remediation/docker/securedockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/step-security/secure-repo/remediation/workflow/pin"
)

var Tr http.RoundTripper = remote.DefaultTransport
Expand All @@ -20,7 +21,11 @@ type SecureDockerfileResponse struct {
DockerfileFetchError bool
}

func SecureDockerFile(inputDockerFile string) (*SecureDockerfileResponse, error) {
type dockerfileConfig struct {
exemptedImages []string
}

func SecureDockerFile(inputDockerFile string, opts ...dockerfileConfig) (*SecureDockerfileResponse, error) {
reader := strings.NewReader(inputDockerFile)
cmds, err := dockerfile.ParseReader(reader)
if err != nil {
Expand All @@ -32,6 +37,12 @@ func SecureDockerFile(inputDockerFile string) (*SecureDockerfileResponse, error)
response.OriginalInput = inputDockerFile
response.IsChanged = false

// Get exempted images list, default to empty if no config provided
var exemptedImages []string
if len(opts) > 0 {
exemptedImages = opts[0].exemptedImages
}

for _, c := range cmds {
if strings.Contains(c.Cmd, "FROM") && strings.Contains(c.Value[0], ":") {
// For being fixable
Expand Down Expand Up @@ -64,6 +75,12 @@ func SecureDockerFile(inputDockerFile string) (*SecureDockerfileResponse, error)
// is already pinned
isPinned = true
}

// Check if image is exempted (skip pinning)
if len(exemptedImages) > 0 && pin.ActionExists(image, exemptedImages) {
continue
}

if !isPinned {
sha, err := getSHA(image, tag)
if err != nil {
Expand Down
24 changes: 18 additions & 6 deletions remediation/docker/securedockerfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@ func TestSecureDockerFile(t *testing.T) {
httpmock.RegisterResponder("GET", "https://index.docker.io/v2/library/python/manifests/3.7", httpmock.NewStringResponder(200, resp))

tests := []struct {
fileName string
isChanged bool
fileName string
isChanged bool
exemptedImages []string
useExemptConfig bool
}{
{fileName: "Dockerfile-not-pinned", isChanged: true},
{fileName: "Dockerfile-not-pinned-as", isChanged: true},
{fileName: "Dockerfile-multiple-images", isChanged: true},
{fileName: "Dockerfile-not-pinned", isChanged: true, useExemptConfig: false},
{fileName: "Dockerfile-not-pinned-as", isChanged: true, useExemptConfig: false},
{fileName: "Dockerfile-multiple-images", isChanged: true, useExemptConfig: false},
{fileName: "Dockerfile-exempted", isChanged: false, exemptedImages: []string{"python"}, useExemptConfig: true},
{fileName: "Dockerfile-exempted-wildcard", isChanged: true, exemptedImages: []string{"amazon*", "alpine"}, useExemptConfig: true},
}

for _, test := range tests {
Expand All @@ -55,7 +59,15 @@ func TestSecureDockerFile(t *testing.T) {
log.Fatal(err)
}

output, err := SecureDockerFile(string(input))
var output *SecureDockerfileResponse
if test.useExemptConfig {
config := dockerfileConfig{
exemptedImages: test.exemptedImages,
}
output, err = SecureDockerFile(string(input), config)
} else {
output, err = SecureDockerFile(string(input))
}
if err != nil {
t.Fatalf("Error not expected: %s", err)
}
Expand Down
39 changes: 36 additions & 3 deletions remediation/workflow/maintainedactions/maintainedActions.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,29 @@ func ReplaceActions(inputYaml string, customerMaintainedActions map[string]strin
}
}
}

// For composite actions
if workflow.Runs.Using == "composite" {
for stepIdx, step := range workflow.Runs.Steps {
if len(step.Uses) > 0 {
actionName := strings.Split(step.Uses, "@")[0]
if newAction, ok := actionMap[actionName]; ok {
latestVersion, err := GetLatestRelease(newAction)
if err != nil {
return inputYaml, updated, fmt.Errorf("unable to get latest release: %v", err)
}
replacements = append(replacements, replacement{
jobName: "composite", // special marker for composite actions
stepIdx: stepIdx,
newAction: newAction,
originalAction: step.Uses,
latestVersion: latestVersion,
})
}
}
}
}

if len(replacements) == 0 {
// No changes needed
return inputYaml, false, nil
Expand All @@ -115,9 +138,19 @@ func ReplaceActions(inputYaml string, customerMaintainedActions map[string]strin

func replaceAction(t *yaml.Node, inputLines []string, replacements []replacement, updated bool) ([]string, bool) {
for _, r := range replacements {
jobsNode := permissions.IterateNode(t, "jobs", "!!map", 0)
jobNode := permissions.IterateNode(jobsNode, r.jobName, "!!map", 0)
stepsNode := permissions.IterateNode(jobNode, "steps", "!!seq", 0)
var stepsNode *yaml.Node

if r.jobName == "composite" {
// Handle composite actions
runsNode := permissions.IterateNode(t, "runs", "!!map", 0)
stepsNode = permissions.IterateNode(runsNode, "steps", "!!seq", 0)
} else {
// Handle regular workflow jobs
jobsNode := permissions.IterateNode(t, "jobs", "!!map", 0)
jobNode := permissions.IterateNode(jobsNode, r.jobName, "!!map", 0)
stepsNode = permissions.IterateNode(jobNode, "steps", "!!seq", 0)
}

if stepsNode == nil {
continue
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ func TestReplaceActions(t *testing.T) {
wantUpdated: true,
wantErr: false,
},
{
name: "composite action with actions to replace",
inputFile: "compositeAction.yml",
outputFile: "compositeAction.yml",
wantUpdated: true,
wantErr: false,
},
}

for _, tt := range tests {
Expand Down
8 changes: 7 additions & 1 deletion remediation/workflow/secureworkflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ func TestSecureWorkflow(t *testing.T) {
{fileName: "multiplejobperms.yml", wantPinnedActions: false, wantAddedHardenRunner: false, wantAddedPermissions: true, wantError: false},
{fileName: "error.yml", wantPinnedActions: false, wantAddedHardenRunner: false, wantAddedPermissions: false, wantError: false},
{fileName: "missingaction.yml", wantPinnedActions: false, wantAddedHardenRunner: false, wantAddedPermissions: false, wantError: true},
{fileName: "compositeAction.yml", wantPinnedActions: true, wantAddedHardenRunner: false, wantAddedPermissions: false, wantAddedMaintainedActions: true, wantError: false},
}
for _, test := range tests {
var err error
Expand Down Expand Up @@ -256,12 +257,17 @@ func TestSecureWorkflow(t *testing.T) {
queryParams["addHardenRunner"] = "true"
queryParams["pinActions"] = "true"
queryParams["addPermissions"] = "false"
case "compositeAction.yml":
queryParams["addMaintainedActions"] = "true"
queryParams["addHardenRunner"] = "false"
queryParams["pinActions"] = "true"
queryParams["addPermissions"] = "false"
}
queryParams["addProjectComment"] = "false"

var output *permissions.SecureWorkflowReponse
var actionMap map[string]string
if test.fileName == "oneJob.yml" {
if test.fileName == "oneJob.yml" || test.fileName == "compositeAction.yml" {
actionMap, err = maintainedactions.LoadMaintainedActions("maintainedactions/maintainedActions.json")
if err != nil {
t.Errorf("unable to load the file %s", err)
Expand Down
7 changes: 7 additions & 0 deletions testfiles/dockerfiles/input/Dockerfile-exempted
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Test file for exempted images
# python should NOT be pinned because it's in the exempted list
FROM python:3.7

RUN apt-get update && apt-get install -y vim

WORKDIR /app
15 changes: 15 additions & 0 deletions testfiles/dockerfiles/input/Dockerfile-exempted-wildcard
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Test file for wildcard exemptions
# amazonlinux should NOT be pinned (matches amazon*)
# alpine should NOT be pinned (exact match)
# python SHOULD be pinned (not exempted)
FROM amazonlinux:2023

RUN yum install -y python3

FROM alpine:3.18

RUN apk add --no-cache bash

FROM python:3.7

RUN pip install requests
7 changes: 7 additions & 0 deletions testfiles/dockerfiles/output/Dockerfile-exempted
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Test file for exempted images
# python should NOT be pinned because it's in the exempted list
FROM python:3.7

RUN apt-get update && apt-get install -y vim

WORKDIR /app
15 changes: 15 additions & 0 deletions testfiles/dockerfiles/output/Dockerfile-exempted-wildcard
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Test file for wildcard exemptions
# amazonlinux should NOT be pinned (matches amazon*)
# alpine should NOT be pinned (exact match)
# python SHOULD be pinned (not exempted)
FROM amazonlinux:2023

RUN yum install -y python3

FROM alpine:3.18

RUN apk add --no-cache bash

FROM python:3.7@sha256:5fb6f4b9d73ddeb0e431c938bee25c69157a1e3c880a81ff72c43a8055628de5

RUN pip install requests
27 changes: 27 additions & 0 deletions testfiles/maintainedActions/input/compositeAction.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: 'Test Composite Action'
description: 'Test composite action for maintained actions replacement'
branding:
icon: 'arrow-up'
color: 'blue'
inputs:
component:
description: 'Component Name'
required: true
runs:
using: 'composite'
steps:
- uses: amannn/action-semantic-pull-request@v5
with:
types: feat,fix,chore

- uses: fkirc/skip-duplicate-actions@v5
with:
do_not_skip: '["release"]'

- uses: chetan/git-restore-mtime-action@v1
with:
pattern: '**/*'

- name: Run custom script
run: echo "Running custom script"
shell: bash
27 changes: 27 additions & 0 deletions testfiles/maintainedActions/output/compositeAction.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: 'Test Composite Action'
description: 'Test composite action for maintained actions replacement'
branding:
icon: 'arrow-up'
color: 'blue'
inputs:
component:
description: 'Component Name'
required: true
runs:
using: 'composite'
steps:
- uses: step-security/action-semantic-pull-request@v5
with:
types: feat,fix,chore

- uses: step-security/skip-duplicate-actions@v5
with:
do_not_skip: '["release"]'

- uses: step-security/git-restore-mtime-action@v2
with:
pattern: '**/*'

- name: Run custom script
run: echo "Running custom script"
shell: bash
25 changes: 25 additions & 0 deletions testfiles/secureworkflow/input/compositeAction.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: 'Test Composite Action'
description: 'Test composite action for secure workflow'
branding:
icon: 'shield'
color: 'blue'
inputs:
token:
description: 'GitHub token'
required: true
runs:
using: 'composite'
steps:
- uses: actions/checkout@v1

- uses: amannn/action-semantic-pull-request@v5
with:
types: feat,fix,chore

- uses: fkirc/skip-duplicate-actions@v5
with:
do_not_skip: '["release"]'

- name: Run script
run: echo "Running script"
shell: bash
25 changes: 25 additions & 0 deletions testfiles/secureworkflow/output/compositeAction.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: 'Test Composite Action'
description: 'Test composite action for secure workflow'
branding:
icon: 'shield'
color: 'blue'
inputs:
token:
description: 'GitHub token'
required: true
runs:
using: 'composite'
steps:
- uses: actions/checkout@v1

- uses: step-security/action-semantic-pull-request@a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 # v5.5.5
with:
types: feat,fix,chore

- uses: step-security/skip-duplicate-actions@b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0a1 # v2.1.0
with:
do_not_skip: '["release"]'

- name: Run script
run: echo "Running script"
shell: bash