diff --git a/.github/workflows/automatePR.yml b/.github/workflows/automatePR.yml index bcc1634a9..62ad28d51 100644 --- a/.github/workflows/automatePR.yml +++ b/.github/workflows/automatePR.yml @@ -16,7 +16,12 @@ jobs: actions: write steps: - - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + - name: Harden Runner + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 + with: + egress-policy: audit + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: repository: step-security/secure-repo diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml deleted file mode 100644 index c40f9f80a..000000000 --- a/.github/workflows/code-review.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Code Review -on: - pull_request: -permissions: - contents: read -jobs: - code-review: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - steps: - - name: Harden Runner - uses: step-security/harden-runner@128a63446a954579617e875aaab7d2978154e969 # v2.4.0 - with: - disable-sudo: true - egress-policy: block - allowed-endpoints: > - api.github.com:443 - int.api.stepsecurity.io:443 - - - name: Code Review - uses: step-security/ai-codewise@int diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 62d466beb..32fc2e5f2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,16 +41,16 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ebacdc22ef6c2cfb85ee5ded8f2e640f4c776dd5 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e0e5ded33cabb451ae0a9768fc7b0410bad9ad44 + uses: github/codeql-action/init@bc02a25f6449997c5e9d5a368879b28f56ae19a1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -63,7 +63,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@e0e5ded33cabb451ae0a9768fc7b0410bad9ad44 + uses: github/codeql-action/autobuild@bc02a25f6449997c5e9d5a368879b28f56ae19a1 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -76,6 +76,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e0e5ded33cabb451ae0a9768fc7b0410bad9ad44 + uses: github/codeql-action/analyze@bc02a25f6449997c5e9d5a368879b28f56ae19a1 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/int.yml b/.github/workflows/int.yml index 5fc4494f0..2562cfc57 100644 --- a/.github/workflows/int.yml +++ b/.github/workflows/int.yml @@ -12,33 +12,51 @@ jobs: publish-test: permissions: contents: read + id-token: write runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ebacdc22ef6c2cfb85ee5ded8f2e640f4c776dd5 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@37335c7bb261b353407cff977110895fa0b4f7d8 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 with: go-version: 1.17 - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@ea7b857d8a33dc2fb4ef5a724500044281b49a5e - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_INT }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_INT }} - aws-region: us-west-2 - run: go test ./... -coverpkg=./... env: PAT: ${{ secrets.PAT }} + - uses: step-security/wait-for-secrets@084b3ae774c0e0003a9307ae4f487c10f1f998fe + id: wait-for-secrets + with: + slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} + secrets: | + AWS_ACCESS_KEY_ID_INT: + name: 'AWS access key id' + description: 'Access key id for secure-repo int' + AWS_SECRET_ACCESS_KEY_INT: + name: 'AWS secret access key' + description: 'Secret access key for secure-repo int' + AWS_SESSION_TOKEN_INT: + name: 'AWS session token' + description: 'Session token for secure-repo int' + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df + with: + aws-access-key-id: ${{ steps.wait-for-secrets.outputs.AWS_ACCESS_KEY_ID_INT }} + aws-secret-access-key: ${{ steps.wait-for-secrets.outputs.AWS_SECRET_ACCESS_KEY_INT }} + aws-session-token: ${{ steps.wait-for-secrets.outputs.AWS_SESSION_TOKEN_INT }} + aws-region: us-west-2 + - name: Deploy to AWS CloudFormation - uses: aws-actions/aws-cloudformation-github-deploy@72bea2c93ca6be253b71b5966ecde13f9e8af2d4 + uses: aws-actions/aws-cloudformation-github-deploy@33527b83bddcf6b3f0b135d9550bde8475325c73 with: name: secure-workflow-api-ecr template: cloudformation/ecr.yml @@ -47,7 +65,7 @@ jobs: - name: Login to Amazon ECR id: login-ecr - uses: aws-actions/amazon-ecr-login@aaf69d68aa3fb14c1d5a6be9ac61fe15b48453a2 + uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 - name: Build, tag, and push image to Amazon ECR env: @@ -59,7 +77,7 @@ jobs: docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - name: Deploy to AWS CloudFormation - uses: aws-actions/aws-cloudformation-github-deploy@72bea2c93ca6be253b71b5966ecde13f9e8af2d4 + uses: aws-actions/aws-cloudformation-github-deploy@33527b83bddcf6b3f0b135d9550bde8475325c73 with: name: secure-workflow-api template: cloudformation/resources.yml diff --git a/.github/workflows/kb-test.yml b/.github/workflows/kb-test.yml index c129faa68..3f2f6c84b 100644 --- a/.github/workflows/kb-test.yml +++ b/.github/workflows/kb-test.yml @@ -14,7 +14,7 @@ jobs: contents: read runs-on: ubuntu-latest steps: - - uses: step-security/harden-runner@ebacdc22ef6c2cfb85ee5ded8f2e640f4c776dd5 # v1 + - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 with: allowed-endpoints: > api.github.com:443 @@ -25,11 +25,11 @@ jobs: objects.githubusercontent.com:443 golang.org:443 - name: Checkout - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: ref: ${{ github.event.pull_request.head.sha }} - name: Set up Go - uses: actions/setup-go@37335c7bb261b353407cff977110895fa0b4f7d8 # v2.1.3 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 with: go-version: 1.17 - name: Run coverage diff --git a/.github/workflows/kbanalysis.yml b/.github/workflows/kbanalysis.yml index 6d846e157..90491fac6 100644 --- a/.github/workflows/kbanalysis.yml +++ b/.github/workflows/kbanalysis.yml @@ -22,11 +22,11 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ebacdc22ef6c2cfb85ee5ded8f2e640f4c776dd5 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 with: egress-policy: audit - - uses: actions/checkout@d0651293c4a5a52e711f25b41b05b2212f385d28 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: repository: step-security/secure-repo diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ec3f2b4a..7b4af1bec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,15 +17,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ebacdc22ef6c2cfb85ee5ded8f2e640f4c776dd5 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@37335c7bb261b353407cff977110895fa0b4f7d8 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 with: go-version: 1.17 @@ -33,7 +33,7 @@ jobs: env: PAT: ${{ secrets.PAT }} - - uses: step-security/wait-for-secrets@1204ba02d7a707c4ef2e906d2ea1e36eebd9bbd2 + - uses: step-security/wait-for-secrets@084b3ae774c0e0003a9307ae4f487c10f1f998fe id: wait-for-secrets with: slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} @@ -49,7 +49,7 @@ jobs: description: 'Session token for secure-repo prod' - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@ea7b857d8a33dc2fb4ef5a724500044281b49a5e + uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df with: aws-access-key-id: ${{ steps.wait-for-secrets.outputs.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ steps.wait-for-secrets.outputs.AWS_SECRET_ACCESS_KEY }} @@ -57,7 +57,7 @@ jobs: aws-region: us-west-2 - name: Deploy to AWS CloudFormation - uses: aws-actions/aws-cloudformation-github-deploy@72bea2c93ca6be253b71b5966ecde13f9e8af2d4 + uses: aws-actions/aws-cloudformation-github-deploy@33527b83bddcf6b3f0b135d9550bde8475325c73 with: name: secure-workflow-api-ecr template: cloudformation/ecr.yml @@ -66,7 +66,7 @@ jobs: - name: Login to Amazon ECR id: login-ecr - uses: aws-actions/amazon-ecr-login@aaf69d68aa3fb14c1d5a6be9ac61fe15b48453a2 + uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 - name: Build, tag, and push image to Amazon ECR env: @@ -78,7 +78,7 @@ jobs: docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - name: Deploy to AWS CloudFormation - uses: aws-actions/aws-cloudformation-github-deploy@72bea2c93ca6be253b71b5966ecde13f9e8af2d4 + uses: aws-actions/aws-cloudformation-github-deploy@33527b83bddcf6b3f0b135d9550bde8475325c73 with: name: secure-workflow-api template: cloudformation/resources.yml diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index d5a7379b3..0b9c84561 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -31,13 +31,18 @@ jobs: # actions: read steps: + - name: Harden Runner + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 + with: + egress-policy: audit + - name: "Checkout code" - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2 + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 with: results_file: results.sarif results_format: sarif @@ -59,7 +64,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: SARIF file path: results.sarif @@ -67,6 +72,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4 + uses: github/codeql-action/upload-sarif@bc02a25f6449997c5e9d5a368879b28f56ae19a1 with: sarif_file: results.sarif diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 869959f5a..7c41255e7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: contents: read runs-on: ubuntu-latest steps: - - uses: step-security/harden-runner@ebacdc22ef6c2cfb85ee5ded8f2e640f4c776dd5 # v1 + - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 with: allowed-endpoints: > api.github.com:443 @@ -30,15 +30,15 @@ jobs: objects.githubusercontent.com:443 golang.org:443 - name: Checkout - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: ref: ${{ github.event.pull_request.head.sha }} - name: Set up Go - uses: actions/setup-go@37335c7bb261b353407cff977110895fa0b4f7d8 # v2.1.3 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 with: go-version: 1.17 - name: Run coverage run: go test ./... -coverpkg=./... -race -coverprofile=coverage.txt -covermode=atomic env: PAT: ${{ secrets.GITHUB_TOKEN }} - - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2 + - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 diff --git a/go.mod b/go.mod index c8158b47c..2beb6dc74 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,9 @@ require ( github.com/aws/aws-lambda-go v1.30.0 github.com/aws/aws-sdk-go v1.43.45 github.com/paulvollmer/dependabot-config-go v0.1.1 - gopkg.in/yaml.v2 v2.4.0 + github.com/sirupsen/logrus v1.8.1 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + gotest.tools v2.2.0+incompatible ) require ( @@ -21,6 +22,7 @@ require ( github.com/goccy/go-json v0.9.7 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-cmp v0.5.7 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect github.com/lestrrat-go/blackmagic v1.0.0 // indirect @@ -32,13 +34,13 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/sirupsen/logrus v1.8.1 // indirect golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) require ( @@ -47,7 +49,7 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/go-containerregistry v0.8.0 github.com/google/go-github/v40 v40.0.0 - github.com/jarcoal/httpmock v1.1.0 + github.com/jarcoal/httpmock v1.4.0 github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/lestrrat-go/jwx v1.2.25 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 diff --git a/go.sum b/go.sum index 49a46a91a..32324feb0 100644 --- a/go.sum +++ b/go.sum @@ -850,8 +850,8 @@ github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6t github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw= github.com/jaguilar/vt100 v0.0.0-20150826170717-2703a27b14ea/go.mod h1:QMdK4dGB3YhEW2BmA1wgGpPYI3HZy/5gD705PXKUVSg= github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= -github.com/jarcoal/httpmock v1.1.0 h1:F47ChZj1Y2zFsCXxNkBPwNNKnAyOATcdQibk0qEdVCE= -github.com/jarcoal/httpmock v1.1.0/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k= +github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a/go.mod h1:xRskid8CManxVta/ALEhJha/pweKBaVG6fWgc0yH25s= github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= @@ -973,6 +973,8 @@ github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb44 github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI= +github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= diff --git a/knowledge-base/actions/angular/dev-infra/github-actions/commit-message-based-labels/action-security.yml b/knowledge-base/actions/angular/dev-infra/github-actions/commit-message-based-labels/action-security.yml deleted file mode 100644 index a024cc35f..000000000 --- a/knowledge-base/actions/angular/dev-infra/github-actions/commit-message-based-labels/action-security.yml +++ /dev/null @@ -1,2 +0,0 @@ -name: 'Breaking Changes Labeling' # angular/dev-infra/github-actions/commit-message-based-labels -# GITHUB_TOKEN not used diff --git a/knowledge-base/actions/angular/dev-infra/github-actions/lock-closed/action-security.yml b/knowledge-base/actions/angular/dev-infra/github-actions/lock-closed/action-security.yml deleted file mode 100644 index 051053cdd..000000000 --- a/knowledge-base/actions/angular/dev-infra/github-actions/lock-closed/action-security.yml +++ /dev/null @@ -1,2 +0,0 @@ -name: 'Lock Closed Issues' # angular/dev-infra/github-actions/lock-closed -# GITHUB_TOKEN not used diff --git a/knowledge-base/actions/angular/dev-infra/github-actions/pull-request-labeling/action-security.yml b/knowledge-base/actions/angular/dev-infra/github-actions/pull-request-labeling/action-security.yml new file mode 100644 index 000000000..130fbbad3 --- /dev/null +++ b/knowledge-base/actions/angular/dev-infra/github-actions/pull-request-labeling/action-security.yml @@ -0,0 +1,2 @@ +name: 'Pull Request Labeling' # angular/dev-infra/github-actions/pull-request-labeling +# GITHUB_TOKEN not used diff --git a/knowledge-base/actions/devbotsxyz/xcode-notarize/action-security.yml b/knowledge-base/actions/devbotsxyz/xcode-notarize/action-security.yml deleted file mode 100644 index 08a078740..000000000 --- a/knowledge-base/actions/devbotsxyz/xcode-notarize/action-security.yml +++ /dev/null @@ -1,2 +0,0 @@ -name: 'Xcode Notarize' # devbotsxyz/xcode-notarize -# GITHUB_TOKEN not used diff --git a/knowledge-base/actions/devbotsxyz/xcode-staple/action-security.yml b/knowledge-base/actions/devbotsxyz/xcode-staple/action-security.yml deleted file mode 100644 index 62790b120..000000000 --- a/knowledge-base/actions/devbotsxyz/xcode-staple/action-security.yml +++ /dev/null @@ -1,2 +0,0 @@ -name: 'Xcode Staple' # devbotsxyz/xcode-staple -# GITHUB_TOKEN not used diff --git a/knowledge-base/actions/homebrew/actions/remove-disabled-formulae/action-security.yml b/knowledge-base/actions/homebrew/actions/remove-disabled-formulae/action-security.yml deleted file mode 100644 index 2ffad3402..000000000 --- a/knowledge-base/actions/homebrew/actions/remove-disabled-formulae/action-security.yml +++ /dev/null @@ -1,2 +0,0 @@ -name: Remove disabled formulae # Homebrew/actions/remove-disabled-formulae -# GITHUB_TOKEN not used \ No newline at end of file diff --git a/knowledge-base/actions/homebrew/actions/remove-disabled-packages/action-security.yml b/knowledge-base/actions/homebrew/actions/remove-disabled-packages/action-security.yml new file mode 100644 index 000000000..5255e85ce --- /dev/null +++ b/knowledge-base/actions/homebrew/actions/remove-disabled-packages/action-security.yml @@ -0,0 +1,2 @@ +name: Remove disabled packages # Homebrew/actions/remove-disabled-packages +# GITHUB_TOKEN not used \ No newline at end of file diff --git a/knowledge-base/actions/tomwillis608/detect-secrets-action/action-security.yml b/knowledge-base/actions/tomwillis608/detect-secrets-action/action-security.yml deleted file mode 100644 index cad824df5..000000000 --- a/knowledge-base/actions/tomwillis608/detect-secrets-action/action-security.yml +++ /dev/null @@ -1,2 +0,0 @@ -name: "Easy detect-secrets" # tomwillis608/detect-secrets-action -# GITHUB_TOKEN not used diff --git a/remediation/dependabot/dependabotconfig.go b/remediation/dependabot/dependabotconfig.go index 26609cd32..33483297e 100644 --- a/remediation/dependabot/dependabotconfig.go +++ b/remediation/dependabot/dependabotconfig.go @@ -105,7 +105,7 @@ func UpdateDependabotConfig(dependabotConfig string) (*UpdateDependabotConfigRes for _, Update := range updateDependabotConfigRequest.Ecosystems { updateAlreadyExist := false for _, update := range configMetadata.Updates { - if update.PackageEcosystem == Update.PackageEcosystem && update.Directory == Update.Directory { + if update.PackageEcosystem == Update.PackageEcosystem && (update.Directory == Update.Directory || update.Directory == Update.Directory+"/") { updateAlreadyExist = true break } diff --git a/remediation/dependabot/dependabotconfig_test.go b/remediation/dependabot/dependabotconfig_test.go index c3f4898da..c4e4f7c78 100644 --- a/remediation/dependabot/dependabotconfig_test.go +++ b/remediation/dependabot/dependabotconfig_test.go @@ -48,6 +48,11 @@ func TestConfigDependabotFile(t *testing.T) { Ecosystems: []Ecosystem{{"npm", "/sample", "daily"}}, isChanged: true, }, + { + fileName: "extra-slash.yml", + Ecosystems: []Ecosystem{{"npm", "/sample", "daily"}}, + isChanged: false, + }, } for _, test := range tests { diff --git a/remediation/workflow/hardenrunner/addaction.go b/remediation/workflow/hardenrunner/addaction.go index 15703d029..2f93eed30 100644 --- a/remediation/workflow/hardenrunner/addaction.go +++ b/remediation/workflow/hardenrunner/addaction.go @@ -12,10 +12,10 @@ import ( const ( HardenRunnerActionPath = "step-security/harden-runner" - HardenRunnerActionName = "Harden Runner" + HardenRunnerActionName = "Harden the runner (Audit all outbound calls)" ) -func AddAction(inputYaml, action string, pinActions bool) (string, bool, error) { +func AddAction(inputYaml, action string, pinActions, pinToImmutable bool, skipContainerJobs bool) (string, bool, error) { workflow := metadata.Workflow{} updated := false err := yaml.Unmarshal([]byte(inputYaml), &workflow) @@ -29,6 +29,10 @@ func AddAction(inputYaml, action string, pinActions bool) (string, bool, error) if metadata.IsCallingReusableWorkflow(job) { continue } + // Skip adding action for jobs running in containers if skipContainerJobs is true + if skipContainerJobs && job.Container.Image != "" { + continue + } alreadyPresent := false for _, step := range job.Steps { if len(step.Uses) > 0 && strings.HasPrefix(step.Uses, HardenRunnerActionPath) { @@ -47,7 +51,7 @@ func AddAction(inputYaml, action string, pinActions bool) (string, bool, error) } if updated && pinActions { - out, _ = pin.PinAction(action, out) + out, _ = pin.PinAction(action, out, nil, pinToImmutable) } return out, updated, nil @@ -61,7 +65,9 @@ func addAction(inputYaml, jobName, action string) (string, error) { return "", fmt.Errorf("unable to parse yaml %v", err) } - jobNode := permissions.IterateNode(&t, jobName, "!!map", 0) + jobNode := permissions.IterateNode(&t, "jobs", "!!map", 0) + + jobNode = permissions.IterateNode(&t, jobName, "!!map", jobNode.Line) jobNode = permissions.IterateNode(&t, "steps", "!!seq", jobNode.Line) diff --git a/remediation/workflow/hardenrunner/addaction_test.go b/remediation/workflow/hardenrunner/addaction_test.go index 4e722f166..3d2ee3e8c 100644 --- a/remediation/workflow/hardenrunner/addaction_test.go +++ b/remediation/workflow/hardenrunner/addaction_test.go @@ -25,6 +25,7 @@ func TestAddAction(t *testing.T) { {name: "already present", args: args{inputYaml: "alreadypresent.yml", action: "step-security/harden-runner@v2"}, want: "alreadypresent.yml", wantErr: false, wantUpdated: true}, {name: "already present 2", args: args{inputYaml: "alreadypresent_2.yml", action: "step-security/harden-runner@v2"}, want: "alreadypresent_2.yml", wantErr: false, wantUpdated: false}, {name: "reusable job", args: args{inputYaml: "reusablejob.yml", action: "step-security/harden-runner@v2"}, want: "reusablejob.yml", wantErr: false, wantUpdated: false}, + {name: "job name in input", args: args{inputYaml: "jobNameInInput.yml", action: "step-security/harden-runner@v2"}, want: "jobNameInInput.yml", wantErr: false, wantUpdated: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -32,7 +33,7 @@ func TestAddAction(t *testing.T) { if err != nil { t.Fatalf("error reading test file") } - got, gotUpdated, err := AddAction(string(input), tt.args.action, false) + got, gotUpdated, err := AddAction(string(input), tt.args.action, false, false, false) if gotUpdated != tt.wantUpdated { t.Errorf("AddAction() updated = %v, wantUpdated %v", gotUpdated, tt.wantUpdated) @@ -52,3 +53,26 @@ func TestAddAction(t *testing.T) { }) } } + +func TestAddActionWithContainer(t *testing.T) { + const inputDirectory = "../../../testfiles/addaction/input" + const outputDirectory = "../../../testfiles/addaction/output" + + // Test container job with skipContainerJobs = true + input, err := ioutil.ReadFile(path.Join(inputDirectory, "container-job.yml")) + if err != nil { + t.Fatalf("error reading test file") + } + + // Test: Skip container jobs when skipContainerJobs = true + got, gotUpdated, err := AddAction(string(input), "step-security/harden-runner@v2", false, false, true) + if err != nil { + t.Errorf("AddAction() with skipContainerJobs=true error = %v", err) + } + if gotUpdated { + t.Errorf("AddAction() with skipContainerJobs=true should not update container job") + } + if got != string(input) { + t.Errorf("AddAction() with skipContainerJobs=true should not modify the yaml") + } +} diff --git a/remediation/workflow/maintainedactions/getlatestrelease.go b/remediation/workflow/maintainedactions/getlatestrelease.go new file mode 100644 index 000000000..af5359b87 --- /dev/null +++ b/remediation/workflow/maintainedactions/getlatestrelease.go @@ -0,0 +1,66 @@ +package maintainedactions + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/google/go-github/v40/github" + "golang.org/x/oauth2" +) + +type Release struct { + TagName string `json:"tag_name"` +} + +func getMajorVersion(version string) string { + hasVPrefix := strings.HasPrefix(version, "v") + version = strings.TrimPrefix(version, "v") + parts := strings.Split(version, ".") + if len(parts) > 0 { + if hasVPrefix { + return "v" + parts[0] + } + return parts[0] + } + if hasVPrefix { + return "v" + version + } + return version +} + +func GetLatestRelease(ownerRepo string) (string, error) { + splitOnSlash := strings.Split(ownerRepo, "/") + if len(splitOnSlash) < 2 { + return "", fmt.Errorf("invalid owner/repo format: %s", ownerRepo) + } + owner := splitOnSlash[0] + repo := splitOnSlash[1] + + ctx := context.Background() + + // First try without token + client := github.NewClient(nil) + release, _, err := client.Repositories.GetLatestRelease(ctx, owner, repo) + if err != nil { + // If failed, try with token + token := os.Getenv("PAT") + if token == "" { + return "", fmt.Errorf("failed to get latest release and no GITHUB_TOKEN available: %w", err) + } + + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + tc := oauth2.NewClient(ctx, ts) + client = github.NewClient(tc) + + release, _, err = client.Repositories.GetLatestRelease(ctx, owner, repo) + if err != nil { + return "", fmt.Errorf("failed to get latest release with token: %w", err) + } + } + + return getMajorVersion(release.GetTagName()), nil +} diff --git a/remediation/workflow/maintainedactions/maintainedActions.go b/remediation/workflow/maintainedactions/maintainedActions.go new file mode 100644 index 000000000..e0d6e3feb --- /dev/null +++ b/remediation/workflow/maintainedactions/maintainedActions.go @@ -0,0 +1,143 @@ +package maintainedactions + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "strings" + + "github.com/step-security/secure-repo/remediation/workflow/metadata" + "github.com/step-security/secure-repo/remediation/workflow/permissions" + "gopkg.in/yaml.v3" +) + +// Action represents a GitHub Action in the maintained actions list +type Action struct { + Name string `json:"name"` + Description string `json:"description"` + ForkedFrom struct { + Name string `json:"name"` + } `json:"forkedFrom"` + Score int `json:"score"` + Image string `json:"image"` +} + +type replacement struct { + jobName string + stepIdx int + newAction string + originalAction string + latestVersion string +} + +// LoadMaintainedActions loads the maintained actions from the JSON file +func LoadMaintainedActions(jsonPath string) (map[string]string, error) { + // Read the JSON file + data, err := ioutil.ReadFile(jsonPath) + if err != nil { + return nil, fmt.Errorf("failed to read maintained actions file: %v", err) + } + + // Parse the JSON + var actions []Action + if err := json.Unmarshal(data, &actions); err != nil { + return nil, fmt.Errorf("failed to parse maintained actions JSON: %v", err) + } + + // Create a map of original actions to their Step Security replacements + actionMap := make(map[string]string) + for _, action := range actions { + if action.ForkedFrom.Name != "" { + actionMap[action.ForkedFrom.Name] = action.Name + } + } + + return actionMap, nil +} + +// ReplaceActions replaces original actions with Step Security actions in a workflow +func ReplaceActions(inputYaml string, customerMaintainedActions map[string]string) (string, bool, error) { + workflow := metadata.Workflow{} + updated := false + + actionMap := customerMaintainedActions + + err := yaml.Unmarshal([]byte(inputYaml), &workflow) + if err != nil { + return "", updated, fmt.Errorf("unable to parse yaml: %v", err) + } + + // Step 1: Check if anything needs to be replaced + + var replacements []replacement + + for jobName, job := range workflow.Jobs { + if metadata.IsCallingReusableWorkflow(job) { + continue + } + for stepIdx, step := range job.Steps { + // fmt.Println("step ", step.Uses) + 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: jobName, + stepIdx: stepIdx, + newAction: newAction, + originalAction: step.Uses, + latestVersion: latestVersion, + }) + } + } + } + if len(replacements) == 0 { + // No changes needed + return inputYaml, false, nil + } + + // Step 2: Now modify the YAML lines manually + t := yaml.Node{} + err = yaml.Unmarshal([]byte(inputYaml), &t) + if err != nil { + return "", updated, fmt.Errorf("unable to parse yaml: %v", err) + } + + inputLines := strings.Split(inputYaml, "\n") + inputLines, updated = replaceAction(&t, inputLines, replacements, updated) + + output := strings.Join(inputLines, "\n") + + return output, updated, nil +} + +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) + if stepsNode == nil { + continue + } + + // Now get the specific step + stepNode := stepsNode.Content[r.stepIdx] + usesNode := permissions.IterateNode(stepNode, "uses", "!!str", 0) + if usesNode == nil { + continue + } + + lineNum := usesNode.Line - 1 // 0-based indexing + columnNum := usesNode.Column - 1 + + // Replace the line + oldLine := inputLines[lineNum] + prefix := oldLine[:columnNum] + inputLines[lineNum] = prefix + r.newAction + "@" + r.latestVersion + updated = true + + } + return inputLines, updated +} diff --git a/remediation/workflow/maintainedactions/maintainedActions.json b/remediation/workflow/maintainedactions/maintainedActions.json new file mode 100644 index 000000000..f155c82d3 --- /dev/null +++ b/remediation/workflow/maintainedactions/maintainedActions.json @@ -0,0 +1,506 @@ +[ + { + "name": "step-security/action-semantic-pull-request", + "description": "A GitHub Action that ensures that your PR title matches the Conventional Commits spec.", + "forkedFrom": { + "name": "amannn/action-semantic-pull-request" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/skip-duplicate-actions", + "description": "Save time and cost when using GitHub Actions", + "forkedFrom": { + "name": "fkirc/skip-duplicate-actions" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/git-restore-mtime-action", + "description": "A GitHub Workflow Action which restores timestamps of files in the current tree", + "forkedFrom": { + "name": "chetan/git-restore-mtime-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/dynamodb-actions", + "description": "Integrate GitHub Action with Amazon DynamoDB", + "forkedFrom": { + "name": "mooyoul/dynamodb-actions" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/publish-unit-test-result-action", + "description": "GitHub Action to publish unit test results on GitHub", + "forkedFrom": { + "name": "EnricoMi/publish-unit-test-result-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/setup-yq", + "description": "Sets up YQ, yet-another-markup-language-query-er, for use in your GitHub Actions workflow", + "forkedFrom": { + "name": "chrisdickinson/setup-yq" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/paths-filter", + "description": "Conditionally run actions based on files modified by PR, feature branch or pushed commits", + "forkedFrom": { + "name": "dorny/paths-filter" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/create-json", + "description": "GitHub Action to create a .json file to use in other steps of the workflow", + "forkedFrom": { + "name": "jsdaniell/create-json" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/npm-get-version-action", + "description": "This Action scans for a package.json file and reads the version number from that", + "forkedFrom": { + "name": "martinbeentjes/npm-get-version-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/change-string-case-action", + "description": "GitHub Action: Make a string lowercase, uppercase, or capitalized", + "forkedFrom": { + "name": "ASzc/change-string-case-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/ghaction-import-gpg", + "description": "GitHub Action to import a GPG key", + "forkedFrom": { + "name": "crazy-max/ghaction-import-gpg" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/conventional-pr-title-action", + "description": "Ensure your PR title matches the Conventional Commits spec", + "forkedFrom": { + "name": "aslafy-z/conventional-pr-title-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/semver-utils", + "description": "One-stop shop for working with semantic versions in your GitHub Actions workflows", + "forkedFrom": { + "name": "madhead/semver-utils" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/pr-labeler-action", + "description": "Automatically labels your PRs based on branch name patterns like feature/* or fix/*", + "forkedFrom": { + "name": "TimonVS/pr-labeler-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/workflow-dispatch", + "description": "A GitHub Action for triggering workflows, using the `workflow_dispatch` event", + "forkedFrom": { + "name": "benc-uk/workflow-dispatch" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/retry", + "description": "Retries a GitHub Action step on failure or timeout", + "forkedFrom": { + "name": "nick-fields/retry" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/action-send-mail", + "description": "A GitHub Action to send an email to multiple recipients", + "forkedFrom": { + "name": "dawidd6/action-send-mail" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/helm-gh-pages", + "description": "A GitHub Action for publishing Helm charts to GitHub Pages", + "forkedFrom": { + "name": "stefanprodan/helm-gh-pages" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/ghaction-setup-docker", + "description": "GitHub Action to set up (download and install) Docker CE", + "forkedFrom": { + "name": "crazy-max/ghaction-setup-docker" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/rust-cache", + "description": "A GitHub Action that implements smart caching for rust/cargo projects", + "forkedFrom": { + "name": "Swatinem/rust-cache" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/nats-action", + "description": "start nats server(s) for GitHub Actions", + "forkedFrom": { + "name": "onichandame/nats-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/foundry-toolchain", + "description": "GitHub action to install Foundry", + "forkedFrom": { + "name": "foundry-rs/foundry-toolchain" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/gh-docker-logs", + "description": "GitHub Action to collect logs from all docker containers", + "forkedFrom": { + "name": "jwalton/gh-docker-logs" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/gh-actions-lua", + "description": "GitHub action for Lua/LuaJIT", + "forkedFrom": { + "name": "leafo/gh-actions-lua" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/vitest-coverage-report-action", + "description": "A GitHub Action to report vitest test coverage results", + "forkedFrom": { + "name": "davelosert/vitest-coverage-report-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/release-notes-generator-action", + "description": "Action to auto generate a release note based on your events", + "forkedFrom": { + "name": "Decathlon/release-notes-generator-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/action-cond", + "description": "Conditional value for GitHub Action - missing expression for GitHub Actions", + "forkedFrom": { + "name": "haya14busa/action-cond" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/action-discord", + "description": "GitHub Action that sends a Discord message.", + "forkedFrom": { + "name": "Ilshidur/action-discord" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/workflow-conclusion-action", + "description": "GitHub action to get workflow conclusion.", + "forkedFrom": { + "name": "technote-space/workflow-conclusion-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/secrets-sync-action", + "description": "A GitHub Action that can sync secrets from one repository to many others.", + "forkedFrom": { + "name": "jpoehnelt/secrets-sync-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/jest-coverage-report-action", + "description": "GitHub action to track your code coverage in every pull request", + "forkedFrom": { + "name": "ArtiomTr/jest-coverage-report-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/google-github-auth", + "description": "A GitHub Action for authenticating to Google Cloud", + "forkedFrom": { + "name": "google-github-actions/auth" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/increment", + "description": "A GitHub Action to increment a repository variable.", + "forkedFrom": { + "name": "action-pack/increment" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/actions-find-and-replace-string", + "description": "A GitHub action to execute find-and-replace on strings", + "forkedFrom": { + "name": "mad9000/actions-find-and-replace-string" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/assign-author", + "description": "GitHub Actions to assign author to issue or PR", + "forkedFrom": { + "name": "technote-space/assign-author" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/setup-gh-cli-action", + "description": "A GitHub action that installs or updates the gh CLI", + "forkedFrom": { + "name": "sersoft-gmbh/setup-gh-cli-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/close-milestone", + "description": "A GitHub action to remove a milestone by the milestone's name", + "forkedFrom": { + "name": "Akkjon/close-milestone" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/ssh-key-action", + "description": "GitHub Action that installs SSH key to .ssh", + "forkedFrom": { + "name": "shimataro/ssh-key-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/actions-hugo", + "description": "GitHub Actions for Hugo âšĄī¸ Setup Hugo quickly and build your site fast. Hugo extended, Hugo Modules, Linux (Ubuntu), macOS, and Windows are supported.", + "forkedFrom": { + "name": "peaceiris/actions-hugo" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/mongodb-github-action", + "description": "Use MongoDB in GitHub Actions", + "forkedFrom": { + "name": "supercharge/mongodb-github-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/action-setup", + "description": "Install pnpm package manager", + "forkedFrom": { + "name": "pnpm/action-setup" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/setup-zig", + "description": "A GitHub action to install a Zig compiler for usage in GitHub Actions workflow", + "forkedFrom": { + "name": "mlugg/setup-zig" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/run-vcpkg", + "description": "A GitHub Action to setup vcpkg for C++ based projects", + "forkedFrom": { + "name": "lukka/run-vcpkg" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/changed-files", + "description": "GitHub action to retrieve all (added, copied, modified, deleted, renamed, type changed, unmerged, unknown) files and directories.", + "forkedFrom": { + "name": "tj-actions/changed-files" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/reviewdog-action-setup", + "description": "Setup reviewdog action", + "forkedFrom": { + "name": "reviewdog/action-setup" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/action-gh-release", + "description": "GitHub Action for creating GitHub Releases", + "forkedFrom": { + "name": "softprops/action-gh-release" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/github-actions-slack", + "description": "GitHub Action for sending message to Slack - With support for Slack's optional arguments", + "forkedFrom": { + "name": "archive/github-actions-slack" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/envsubst-action", + "description": "GitHub Action for envsubst", + "forkedFrom": { + "name": "danielr1996/envsubst-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/test-reporter", + "description": "Displays test results from popular testing frameworks directly in GitHub", + "forkedFrom": { + "name": "dorny/test-reporter" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/github-action-get-latest-release", + "description": "A GitHub action to get the latest release from another repository", + "forkedFrom": { + "name": "pozetroninc/github-action-get-latest-release" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/runs-on-cache", + "description": "Shockingly faster GitHub Action cache with S3 backend", + "forkedFrom": { + "name": "runs-on/cache" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/multi-labeler", + "description": "Multi labeler for title, body, comments, commit messages, branch, author or files with automated status checks", + "forkedFrom": { + "name": "fuxingloh/multi-labeler" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/background-action", + "description": "Background commands with log tailing/capture; waits until file/port/socket/http are ready to proceed. Isolates/dedupe errors", + "forkedFrom": { + "name": "JarvusInnovations/background-action" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/add-pr-comment", + "description": "GitHub Action which adds a comment to a pull request's issue", + "forkedFrom": { + "name": "mshick/add-pr-comment" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/ghaction-setup-containerd", + "description": "GitHub Action to set up containerd", + "forkedFrom": { + "name": "crazy-max/ghaction-setup-containerd" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + }, + { + "name": "step-security/actions-cache/restore", + "description": "GitHub Action to restore cache", + "forkedFrom": { + "name": "tespkg/actions-cache/restore" + }, + "score": 10, + "image": "https://avatars.githubusercontent.com/u/88700172?v=4" + } +] \ No newline at end of file diff --git a/remediation/workflow/maintainedactions/maintainedactions_test.go b/remediation/workflow/maintainedactions/maintainedactions_test.go new file mode 100644 index 000000000..ffbb7f46b --- /dev/null +++ b/remediation/workflow/maintainedactions/maintainedactions_test.go @@ -0,0 +1,120 @@ +package maintainedactions + +import ( + "io/ioutil" + "path" + "testing" + + "github.com/jarcoal/httpmock" +) + +func TestReplaceActions(t *testing.T) { + const inputDirectory = "../../../testfiles/maintainedActions/input" + const outputDirectory = "../../../testfiles/maintainedActions/output" + + // Activate httpmock + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + // Mock GitHub API responses for getting latest releases + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/action-semantic-pull-request/releases/latest", + httpmock.NewStringResponder(200, `{ + "tag_name": "v5.5.5", + "name": "v5.5.5", + "body": "Release notes", + "created_at": "2023-01-01T00:00:00Z" + }`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/skip-duplicate-actions/releases/latest", + httpmock.NewStringResponder(200, `{ + "tag_name": "v5.3.2", + "name": "v5.3.2", + "body": "Release notes", + "created_at": "2023-01-01T00:00:00Z" + }`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/git-restore-mtime-action/releases/latest", + httpmock.NewStringResponder(200, `{ + "tag_name": "v2.1.0", + "name": "v2.1.0", + "body": "Release notes", + "created_at": "2023-01-01T00:00:00Z" + }`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/actions-cache/releases/latest", + httpmock.NewStringResponder(200, `{ + "tag_name": "v1.0.0", + "name": "v1.0.0", + "body": "Release notes", + "created_at": "2023-01-01T00:00:00Z" + }`)) + + tests := []struct { + name string + inputFile string + outputFile string + wantUpdated bool + wantErr bool + }{ + { + name: "one job with actions to replace", + inputFile: "oneJob.yml", + outputFile: "oneJob.yml", + wantUpdated: true, + wantErr: false, + }, + { + name: "no changes needed - already using maintained actions", + inputFile: "noChangesNeeded.yml", + outputFile: "noChangesNeeded.yml", + wantUpdated: false, + wantErr: false, + }, + { + name: "double job with actions to replace", + inputFile: "doubleJob.yml", + outputFile: "doubleJob.yml", + wantUpdated: true, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Read input file + input, err := ioutil.ReadFile(path.Join(inputDirectory, tt.inputFile)) + if err != nil { + t.Fatalf("error reading input file: %v", err) + } + actionMap, err := LoadMaintainedActions("maintainedActions.json") + if err != nil { + t.Errorf("ReplaceActions() unable to json file %v", err) + return + } + got, updated, replaceErr := ReplaceActions(string(input), actionMap) + + // Check error + if (replaceErr != nil) != tt.wantErr { + t.Errorf("ReplaceActions() error = %v, wantErr %v", replaceErr, tt.wantErr) + return + } + + // Check if updated flag matches + if updated != tt.wantUpdated { + t.Errorf("ReplaceActions() updated = %v, wantUpdated %v", updated, tt.wantUpdated) + } + + // Read expected output file + expectedOutput, err := ioutil.ReadFile(path.Join(outputDirectory, tt.outputFile)) + if err != nil { + t.Fatalf("error reading expected output file: %v", err) + } + + // Compare output with expected + if got != string(expectedOutput) { + // WriteYAML(tt.outputFile+"second", got) + t.Errorf("ReplaceActions() = %v, want %v", got, string(expectedOutput)) + } + }) + } +} diff --git a/remediation/workflow/metadata/actionmetadata.go b/remediation/workflow/metadata/actionmetadata.go index 1fb203025..506f0debb 100644 --- a/remediation/workflow/metadata/actionmetadata.go +++ b/remediation/workflow/metadata/actionmetadata.go @@ -30,10 +30,18 @@ type Step struct { type Job struct { Permissions Permissions `yaml:"permissions"` Uses string `yaml:"uses"` + Env Env `yaml:"env"` + Container Container `yaml:"container"` // RunsOn []string `yaml:"runs-on"` Steps []Step `yaml:"steps"` } +type Container struct { + Image string `yaml:"image"` + Options string `yaml:"options"` + Env Env `yaml:"env"` +} + type Jobs map[string]Job type With map[string]string type Env map[string]string diff --git a/remediation/workflow/metadata/actionmetadata_test.go b/remediation/workflow/metadata/actionmetadata_test.go index d3967e411..2696baa7c 100644 --- a/remediation/workflow/metadata/actionmetadata_test.go +++ b/remediation/workflow/metadata/actionmetadata_test.go @@ -181,6 +181,7 @@ func TestKnowledgeBase(t *testing.T) { func doesActionRepoExist(filePath string) bool { splitOnSlash := strings.Split(filePath, "/") + owner := splitOnSlash[5] repo := splitOnSlash[6] diff --git a/remediation/workflow/permissions/permissions.go b/remediation/workflow/permissions/permissions.go index b26d63a37..018eea47b 100644 --- a/remediation/workflow/permissions/permissions.go +++ b/remediation/workflow/permissions/permissions.go @@ -12,18 +12,19 @@ import ( ) type SecureWorkflowReponse struct { - OriginalInput string - FinalOutput string - IsChanged bool - HasErrors bool - AlreadyHasPermissions bool - PinnedActions bool - AddedHardenRunner bool - AddedPermissions bool - IncorrectYaml bool - WorkflowFetchError bool - JobErrors []JobError - MissingActions []string + OriginalInput string + FinalOutput string + IsChanged bool + HasErrors bool + AlreadyHasPermissions bool + AddedMaintainedActions bool + PinnedActions bool + AddedHardenRunner bool + AddedPermissions bool + IncorrectYaml bool + WorkflowFetchError bool + JobErrors []JobError + MissingActions []string } type JobError struct { @@ -38,6 +39,7 @@ const errorMissingAction = "KnownIssue-4: Action %s is not in the knowledge base const errorAlreadyHasPermissions = "KnownIssue-5: Permissions were not added to the job since it already had permissions defined" const errorDockerAction = "KnownIssue-6: Action %s is a docker action which uses Github token. Docker actions that uses token are not supported" const errorReusableWorkflow = "KnownIssue-7: Action %s is a reusable workflow. Reusable workflows are not supported as of now." +const errorGithubTokenInJobEnv = "KnownIssue-8: Permissions were not added to the jobs since it has GITHUB_TOKEN in job level env variable" const errorIncorrectYaml = "Unable to parse the YAML workflow file" // To avoid a typo while adding the permissions @@ -78,7 +80,16 @@ func alreadyHasWorkflowPermissions(workflow metadata.Workflow) bool { return workflow.Permissions.IsSet } -func AddWorkflowLevelPermissions(inputYaml string, addProjectComment bool) (string, error) { +func githubTokenInJobLevelEnv(job metadata.Job) bool { + for _, envValue := range job.Env { + if strings.Contains(envValue, "secrets.GITHUB_TOKEN") || strings.Contains(envValue, "github.token") { + return true + } + } + return false +} + +func AddWorkflowLevelPermissions(inputYaml string, addProjectComment bool, addEmptyTopLevelPermissions bool) (string, error) { workflow := metadata.Workflow{} err := yaml.Unmarshal([]byte(inputYaml), &workflow) @@ -127,13 +138,20 @@ func AddWorkflowLevelPermissions(inputYaml string, addProjectComment bool) (stri spaces += " " } - if addProjectComment { - output = append(output, spaces+"permissions: # added using https://github.com/step-security/secure-repo") + if addEmptyTopLevelPermissions { + if addProjectComment { + output = append(output, spaces+"permissions: {} # added using https://github.com/step-security/secure-repo") + } else { + output = append(output, spaces+"permissions: {}") + } } else { - output = append(output, spaces+"permissions:") + if addProjectComment { + output = append(output, spaces+"permissions: # added using https://github.com/step-security/secure-repo") + } else { + output = append(output, spaces+"permissions:") + } + output = append(output, spaces+" contents: read") } - - output = append(output, spaces+" contents: read") output = append(output, "") for i := line - 1; i < len(inputLines); i++ { @@ -143,7 +161,7 @@ func AddWorkflowLevelPermissions(inputYaml string, addProjectComment bool) (stri return strings.Join(output, "\n"), nil } -func AddJobLevelPermissions(inputYaml string) (*SecureWorkflowReponse, error) { +func AddJobLevelPermissions(inputYaml string, addEmptyTopLevelPermissions bool) (*SecureWorkflowReponse, error) { workflow := metadata.Workflow{} errors := make(map[string][]string) @@ -177,6 +195,12 @@ func AddJobLevelPermissions(inputYaml string) (*SecureWorkflowReponse, error) { continue } + if githubTokenInJobLevelEnv(job) { + fixWorkflowPermsReponse.HasErrors = true + errors[jobName] = append(errors[jobName], errorGithubTokenInJobEnv) + continue + } + if metadata.IsCallingReusableWorkflow(job) { fixWorkflowPermsReponse.HasErrors = true errors[jobName] = append(errors[jobName], fmt.Sprintf(errorReusableWorkflow, job.Uses)) @@ -199,7 +223,7 @@ func AddJobLevelPermissions(inputYaml string) (*SecureWorkflowReponse, error) { if strings.Compare(inputYaml, fixWorkflowPermsReponse.FinalOutput) != 0 { fixWorkflowPermsReponse.IsChanged = true - if len(perms) == 1 && strings.Contains(perms[0], contents_read) { + if len(perms) == 1 && strings.Contains(perms[0], contents_read) && !addEmptyTopLevelPermissions { // Don't add contents: read, because it will get defined at workflow level continue } else { diff --git a/remediation/workflow/permissions/permissions_test.go b/remediation/workflow/permissions/permissions_test.go index 66ccca031..993bbdcd6 100644 --- a/remediation/workflow/permissions/permissions_test.go +++ b/remediation/workflow/permissions/permissions_test.go @@ -18,6 +18,11 @@ func TestAddJobLevelPermissions(t *testing.T) { } for _, f := range files { + + if f.Name() == "empty-top-level-permissions.yml" { + continue + } + input, err := ioutil.ReadFile(path.Join(inputDirectory, f.Name())) if err != nil { @@ -26,7 +31,7 @@ func TestAddJobLevelPermissions(t *testing.T) { os.Setenv("KBFolder", "../../../knowledge-base/actions") - fixWorkflowPermsResponse, err := AddJobLevelPermissions(string(input)) + fixWorkflowPermsResponse, err := AddJobLevelPermissions(string(input), false) output := fixWorkflowPermsResponse.FinalOutput jobErrors := fixWorkflowPermsResponse.JobErrors @@ -68,6 +73,47 @@ func TestAddJobLevelPermissions(t *testing.T) { } } +func TestAddJobLevelPermissionsWithEmptyTopLevel(t *testing.T) { + const inputDirectory = "../../../testfiles/joblevelpermskb/input" + const outputDirectory = "../../../testfiles/joblevelpermskb/output" + + // Test the empty-top-level-permissions.yml file + input, err := ioutil.ReadFile(path.Join(inputDirectory, "empty-top-level-permissions.yml")) + if err != nil { + t.Fatal(err) + } + + expectedOutput, err := ioutil.ReadFile(path.Join(outputDirectory, "empty-top-level-permissions.yml")) + if err != nil { + t.Fatal(err) + } + + os.Setenv("KBFolder", "../../../knowledge-base/actions") + + // Test with addEmptyTopLevelPermissions = true + fixWorkflowPermsResponse, err := AddJobLevelPermissions(string(input), true) + if err != nil { + t.Errorf("Unexpected error with addEmptyTopLevelPermissions=true: %v", err) + } + + if fixWorkflowPermsResponse.FinalOutput != string(expectedOutput) { + t.Errorf("test failed with addEmptyTopLevelPermissions=true for empty-top-level-permissions.yml\nExpected:\n%s\n\nGot:\n%s", + string(expectedOutput), fixWorkflowPermsResponse.FinalOutput) + } + + // Test with addEmptyTopLevelPermissions = false (should skip contents: read) + fixWorkflowPermsResponse2, err2 := AddJobLevelPermissions(string(input), false) + if err2 != nil { + t.Errorf("Unexpected error with addEmptyTopLevelPermissions=false: %v", err2) + } + + // With false, contents: read should be skipped at job level + if fixWorkflowPermsResponse2.FinalOutput != string(input) { + t.Errorf("test failed with addEmptyTopLevelPermissions=false for empty-top-level-permissions.yml\nExpected:\n%s\n\nGot:\n%s", + string(input), fixWorkflowPermsResponse2.FinalOutput) + } +} + func Test_addPermissions(t *testing.T) { type args struct { inputYaml string @@ -112,6 +158,10 @@ func TestAddWorkflowLevelPermissions(t *testing.T) { continue } + if f.Name() == "empty-permissions.yml" { + continue + } + input, err := ioutil.ReadFile(path.Join(inputDirectory, f.Name())) if err != nil { @@ -125,7 +175,7 @@ func TestAddWorkflowLevelPermissions(t *testing.T) { addProjectComment = true } - output, err := AddWorkflowLevelPermissions(string(input), addProjectComment) + output, err := AddWorkflowLevelPermissions(string(input), addProjectComment, false) if err != nil { t.Errorf("Error not expected") @@ -143,3 +193,41 @@ func TestAddWorkflowLevelPermissions(t *testing.T) { } } + +func TestAddWorkflowLevelPermissionsWithEmpty(t *testing.T) { + const inputDirectory = "../../../testfiles/toplevelperms/input" + const outputDirectory = "../../../testfiles/toplevelperms/output" + + // Test the empty-permissions.yml file + input, err := ioutil.ReadFile(path.Join(inputDirectory, "empty-permissions.yml")) + if err != nil { + t.Fatal(err) + } + + expectedOutput, err := ioutil.ReadFile(path.Join(outputDirectory, "empty-permissions.yml")) + if err != nil { + t.Fatal(err) + } + + // Test with addEmptyTopLevelPermissions = true + output, err := AddWorkflowLevelPermissions(string(input), false, true) + if err != nil { + t.Errorf("Unexpected error with addEmptyTopLevelPermissions=true: %v", err) + } + + if output != string(expectedOutput) { + t.Errorf("test failed with addEmptyTopLevelPermissions=true for empty-permissions.yml\nExpected:\n%s\n\nGot:\n%s", + string(expectedOutput), output) + } + + // Test with addEmptyTopLevelPermissions = false (should add contents: read) + output2, err2 := AddWorkflowLevelPermissions(string(input), false, false) + if err2 != nil { + t.Errorf("Unexpected error with addEmptyTopLevelPermissions=false: %v", err2) + } + + // With false, should add contents: read instead of empty permissions + if !strings.Contains(output2, "contents: read") || strings.Contains(output2, "permissions: {}") { + t.Errorf("test failed with addEmptyTopLevelPermissions=false for empty-permissions.yml - should contain 'contents: read' but not 'permissions: {}'\nGot:\n%s", output2) + } +} diff --git a/remediation/workflow/pin/action_image_manifest.go b/remediation/workflow/pin/action_image_manifest.go new file mode 100644 index 000000000..16e3a1ad8 --- /dev/null +++ b/remediation/workflow/pin/action_image_manifest.go @@ -0,0 +1,120 @@ +package pin + +import ( + "encoding/json" + "fmt" + "regexp" + "strings" + + "net/http" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/sirupsen/logrus" +) + +var ( + githubImmutableActionArtifactType = "application/vnd.github.actions.package.v1+json" + semanticTagRegex = regexp.MustCompile(`v[0-9]+\.[0-9]+\.[0-9]+$`) +) + +type ociManifest struct { + ArtifactType string `json:"artifactType"` +} + +// isImmutableAction checks if the action is an immutable action or not +// It queries the OCI manifest for the action and checks if the artifact type is "application/vnd.github.actions.package.v1+json" +// +// Example usage: +// +// # Immutable action (returns true) +// isImmutableAction("actions/checkout@v4.2.2") +// +// # Non-Immutable action (returns false) +// isImmutableAction("actions/checkout@v4.2.3") +// +// REF - https://github.com/actions/publish-immutable-action/issues/216#issuecomment-2549914784 +func IsImmutableAction(action string) bool { + + artifactType, err := getOCIImageArtifactTypeForGhAction(action) + if err != nil { + // log the error + logrus.WithFields(logrus.Fields{"action": action}).WithError(err).Error("error in getting OCI manifest for image") + return false + } + + if artifactType == githubImmutableActionArtifactType { + return true + } + return false + +} + +// getOCIImageArtifactTypeForGhAction retrieves the artifact type from a GitHub Action's OCI manifest. +// This function is used to determine if an action is immutable by checking its artifact type. +// +// Example usage: +// +// # Immutable action (returns "application/vnd.github.actions.package.v1+json", nil) +// artifactType, err := getOCIImageArtifactTypeForGhAction("actions/checkout@v4.2.2") +// +// Returns: +// - artifactType: The artifact type string from the OCI manifest +// - error: An error if the action format is invalid or if there's a problem retrieving the manifest +func getOCIImageArtifactTypeForGhAction(action string) (string, error) { + + // Split the action into parts (e.g., "actions/checkout@v2" -> ["actions/checkout", "v2"]) + parts := strings.Split(action, "@") + if len(parts) != 2 { + return "", fmt.Errorf("invalid action format") + } + + // For bundled actions like github/codeql-action/analyze@v3, + // we only need the repository part (github/codeql-action) to check for immutability + actionPath := parts[0] + if strings.Count(parts[0], "/") > 1 { + pathParts := strings.Split(parts[0], "/") + actionPath = strings.Join(pathParts[:2], "/") + } + + // convert v1.x.x to 1.x.x which is + // use regexp to match tag version format and replace v in prefix + // as immutable actions image tag is in format 1.x.x (without v prefix) + // REF - https://github.com/actions/publish-immutable-action/issues/216#issuecomment-2549914784 + if semanticTagRegex.MatchString(parts[1]) { + // v1.x.x -> 1.x.x + parts[1] = strings.TrimPrefix(parts[1], "v") + } + + // Convert GitHub action to GHCR image reference using proper OCI reference format + image := fmt.Sprintf("ghcr.io/%s:%s", actionPath, parts[1]) + imageManifest, err := getOCIManifestForImage(image) + if err != nil { + return "", err + } + + var ociManifest ociManifest + err = json.Unmarshal([]byte(imageManifest), &ociManifest) + if err != nil { + return "", err + } + return ociManifest.ArtifactType, nil +} + +// getOCIManifestForImage retrieves the artifact type from the OCI image manifest +func getOCIManifestForImage(imageRef string) (string, error) { + + // Parse the image reference + ref, err := name.ParseReference(imageRef) + if err != nil { + return "", fmt.Errorf("error parsing reference: %v", err) + } + + // Get the image manifest + desc, err := remote.Get(ref, remote.WithTransport(http.DefaultTransport)) + if err != nil { + return "", fmt.Errorf("error getting manifest: %v", err) + } + + return string(desc.Manifest), nil +} diff --git a/remediation/workflow/pin/action_image_manifest_test.go b/remediation/workflow/pin/action_image_manifest_test.go new file mode 100644 index 000000000..d16af7de3 --- /dev/null +++ b/remediation/workflow/pin/action_image_manifest_test.go @@ -0,0 +1,163 @@ +package pin + +import ( + "crypto/tls" + "io/ioutil" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "testing" +) + +type customTransport struct { + base http.RoundTripper + baseURL string +} + +func (t *customTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if strings.Contains(req.URL.Host, "ghcr.io") { + req2 := req.Clone(req.Context()) + req2.URL.Scheme = "https" + req2.URL.Host = strings.TrimPrefix(t.baseURL, "https://") + return t.base.RoundTrip(req2) + } + return t.base.RoundTrip(req) +} + +func createGhesTestServer(t *testing.T) *httptest.Server { + return httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json") + + if !strings.Contains(r.Host, "ghcr.io") { + w.WriteHeader(http.StatusNotFound) + return + } + // Mock manifest endpoints + switch r.URL.Path { + + case "/v2/": // simulate ping request + w.WriteHeader(http.StatusOK) + + case "/token": + // for immutable actions, since image will be present in registry...it returns 200 OK with token + // otherwise it returns 403 Forbidden + scope := r.URL.Query().Get("scope") + switch scope { + case "repository:actions/checkout:pull": + fallthrough + case "repository:step-security/wait-for-secrets:pull": + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"token": "test-token", "access_token": "test-token"}`)) + default: + w.WriteHeader(http.StatusForbidden) + w.Write([]byte(`{"errors": [{"code": "DENIED", "message": "requested access to the resource is denied"}]}`)) + } + + case "/v2/actions/checkout/manifests/4.2.2": + fallthrough + case "/v2/actions/checkout/manifests/1.2.0": + fallthrough + case "/v2/step-security/wait-for-secrets/manifests/1.2.0": + w.Write(readHttpResponseForAction(t, r.URL.Path)) + case "/v2/actions/checkout/manifests/1.2.3": // since this version doesn't exist + fallthrough + default: + w.WriteHeader(http.StatusNotFound) + w.Write(readHttpResponseForAction(t, "default")) + } + })) +} + +func Test_isImmutableAction(t *testing.T) { + // Create test server that mocks GitHub Container Registry + server := createGhesTestServer(t) + defer server.Close() + + // Create a custom client that redirects ghcr.io to our test server + originalClient := http.DefaultClient + http.DefaultClient = &http.Client{ + Transport: &customTransport{ + base: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + baseURL: server.URL, + }, + } + + // update default transport + OriginalTransport := http.DefaultTransport + http.DefaultTransport = http.DefaultClient.Transport + + defer func() { + http.DefaultClient = originalClient + http.DefaultTransport = OriginalTransport + }() + + tests := []struct { + name string + action string + want bool + }{ + { + name: "immutable action - 1", + action: "actions/checkout@v4.2.2", + want: true, + }, + { + name: "immutable action - 2", + action: "step-security/wait-for-secrets@v1.2.0", + want: true, + }, + { + name: "non immutable action(valid action)", + action: "sailikhith-stepsecurity/hello-action@v1.0.2", + want: false, + }, + { + name: "non immutable action(invalid action)", + action: "sailikhith-stepsecurity/no-such-action@v1.0.2", + want: false, + }, + { + name: " action with release tag doesn't exist", + action: "actions/checkout@1.2.3", + want: false, + }, + { + name: "invalid action format", + action: "invalid-format", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + got := IsImmutableAction(tt.action) + if got != tt.want { + t.Errorf("isImmutableAction() = %v, want %v", got, tt.want) + } + }) + } +} + +func readHttpResponseForAction(t *testing.T, actionPath string) []byte { + // remove v2 prefix from action path + actionPath = strings.TrimPrefix(actionPath, "/v2/") + + fileName := strings.ReplaceAll(actionPath, "/", "-") + ".json" + testFilesDir := "../../../testfiles/pinactions/immutableActionResponses/" + respFilePath := filepath.Join(testFilesDir, fileName) + + resp, err := ioutil.ReadFile(respFilePath) + if err != nil { + t.Fatalf("error reading test file:%v", err) + } + + return resp +} diff --git a/remediation/workflow/pin/pinactions.go b/remediation/workflow/pin/pinactions.go index b8aef31c5..b35d6e15a 100644 --- a/remediation/workflow/pin/pinactions.go +++ b/remediation/workflow/pin/pinactions.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "os" + "path/filepath" + "regexp" "strings" "github.com/google/go-github/v40/github" @@ -12,7 +14,7 @@ import ( "gopkg.in/yaml.v3" ) -func PinActions(inputYaml string) (string, bool, error) { +func PinActions(inputYaml string, exemptedActions []string, pinToImmutable bool) (string, bool, error) { workflow := metadata.Workflow{} updated := false err := yaml.Unmarshal([]byte(inputYaml), &workflow) @@ -27,7 +29,7 @@ func PinActions(inputYaml string) (string, bool, error) { for _, step := range job.Steps { if len(step.Uses) > 0 { localUpdated := false - out, localUpdated = PinAction(step.Uses, out) + out, localUpdated = PinAction(step.Uses, out, exemptedActions, pinToImmutable) updated = updated || localUpdated } } @@ -36,19 +38,24 @@ func PinActions(inputYaml string) (string, bool, error) { return out, updated, nil } -func PinAction(action, inputYaml string) (string, bool) { +func PinAction(action, inputYaml string, exemptedActions []string, pinToImmutable bool) (string, bool) { updated := false if !strings.Contains(action, "@") || strings.HasPrefix(action, "docker://") { return inputYaml, updated // Cannot pin local actions and docker actions } - if isAbsolute(action) { + if isAbsolute(action) || (pinToImmutable && IsImmutableAction(action)) { return inputYaml, updated } leftOfAt := strings.Split(action, "@") tagOrBranch := leftOfAt[1] + // skip pinning for exempted actions + if ActionExists(leftOfAt[0], exemptedActions) { + return inputYaml, updated + } + splitOnSlash := strings.Split(leftOfAt[0], "/") owner := splitOnSlash[0] repo := splitOnSlash[1] @@ -73,13 +80,67 @@ func PinAction(action, inputYaml string) (string, bool) { return inputYaml, updated } - pinnedAction := fmt.Sprintf("%s@%s # %s", leftOfAt[0], commitSHA, tagOrBranch) - updated = !strings.EqualFold(action, pinnedAction) - inputYaml = strings.ReplaceAll(inputYaml, action, pinnedAction) - yamlWithPreviousActionCommentsRemoved, wasModified := removePreviousActionComments(pinnedAction, inputYaml) - if wasModified { - return yamlWithPreviousActionCommentsRemoved, updated + // pinnedAction := fmt.Sprintf("%s@%s # %s", leftOfAt[0], commitSHA, tagOrBranch) + // build separately so we can quote only the ref, not the comment + pinnedRef := fmt.Sprintf("%s@%s", leftOfAt[0], commitSHA) + comment := fmt.Sprintf(" # %s", tagOrBranch) + fullPinned := pinnedRef + comment + + // if the action with version is immutable, then pin the action with version instead of sha + pinnedActionWithVersion := fmt.Sprintf("%s@%s", leftOfAt[0], tagOrBranch) + if pinToImmutable && semanticTagRegex.MatchString(tagOrBranch) && IsImmutableAction(pinnedActionWithVersion) { + // strings.ReplaceAll is not suitable here because it would incorrectly replace substrings + // For example, if we want to replace "actions/checkout@v1" to "actions/checkout@v1.2.3", it would also incorrectly match and replace in "actions/checkout@v1.2.3" + // making new string to "actions/checkout@v1.2.3.2.3" + // + // Instead, we use a regex pattern that ensures we only replace complete action references: + // Pattern: (@)($|\s|"|') + // - Group 1 (@): Captures the exact action reference + // - Group 2 ($|\s|"|'): Captures the delimiter that follows (end of line, whitespace, or quotes) + // + // Examples: + // - "actions/checkout@v1.2.3" - No match (no delimiter after v1) + // - "actions/checkout@v1 " - Matches (space delimiter) + // - "actions/checkout@v1"" - Matches (quote delimiter) + // - "actions/checkout@v1" - Matches (quote delimiter) + // - "actions/checkout@v1\n" - Matches (newline is considered whitespace \s) + + actionRegex := regexp.MustCompile(`(` + regexp.QuoteMeta(action) + `)($|\s|"|')`) + inputYaml = actionRegex.ReplaceAllString(inputYaml, pinnedActionWithVersion+"$2") + + inputYaml, _ = removePreviousActionComments(pinnedActionWithVersion, inputYaml) + return inputYaml, !strings.EqualFold(action, pinnedActionWithVersion) } + + updated = !strings.EqualFold(action, fullPinned) + + // 1) Double-quoted form: "owner/repo@oldRef" + doubleQuotedPattern := `"` + regexp.QuoteMeta(action) + `"` + `($|\s|"|')` + doubleQuotedRe := regexp.MustCompile(doubleQuotedPattern) + inputYaml = doubleQuotedRe.ReplaceAllString( + inputYaml, + fmt.Sprintf(`"%s"%s$1`, pinnedRef, comment), + ) + inputYaml, _ = removePreviousActionComments(fmt.Sprintf(`"%s"%s`, pinnedRef, comment), inputYaml) + + // 2) Single-quoted form: 'owner/repo@oldRef' + singleQuotedPattern := `'` + regexp.QuoteMeta(action) + `'` + `($|\s|"|')` + singleQuotedRe := regexp.MustCompile(singleQuotedPattern) + inputYaml = singleQuotedRe.ReplaceAllString( + inputYaml, + fmt.Sprintf(`'%s'%s$1`, pinnedRef, comment), + ) + inputYaml, _ = removePreviousActionComments(fmt.Sprintf(`'%s'%s`, pinnedRef, comment), inputYaml) + + // 3) Unquoted form: owner/repo@oldRef + unqPattern := `\b` + regexp.QuoteMeta(action) + `\b` + `($|\s|"|')` + unqRe := regexp.MustCompile(unqPattern) + inputYaml = unqRe.ReplaceAllString( + inputYaml, + fullPinned+`$1`, + ) + inputYaml, _ = removePreviousActionComments(fullPinned, inputYaml) + return inputYaml, updated } @@ -95,11 +156,12 @@ func removePreviousActionComments(pinnedAction, inputYaml string) (string, bool) inputYaml = stringParts[0] for idx := 1; idx < len(stringParts); idx++ { trimmedString := strings.SplitN(stringParts[idx], "\n", 2) + inputYaml = inputYaml + pinnedAction if len(trimmedString) > 1 { if strings.Contains(trimmedString[0], "#") { updated = true } - inputYaml = inputYaml + pinnedAction + "\n" + trimmedString[1] + inputYaml = inputYaml + "\n" + trimmedString[1] } } } @@ -163,3 +225,20 @@ func getSemanticVersion(client *github.Client, owner, repo, tagOrBranch, commitS } return tagOrBranch, nil } + +// Function to check if an action matches any pattern in the list +func ActionExists(actionName string, patterns []string) bool { + for _, pattern := range patterns { + // Use filepath.Match to match the pattern + matched, err := filepath.Match(pattern, actionName) + if err != nil { + // Handle invalid patterns + fmt.Printf("Error matching pattern: %v\n", err) + continue + } + if matched { + return true + } + } + return false +} diff --git a/remediation/workflow/pin/pinactions_test.go b/remediation/workflow/pin/pinactions_test.go index a4872be77..37a842cbe 100644 --- a/remediation/workflow/pin/pinactions_test.go +++ b/remediation/workflow/pin/pinactions_test.go @@ -3,7 +3,9 @@ package pin import ( "io/ioutil" "log" + "net/http" "path" + "strings" "testing" "github.com/jarcoal/httpmock" @@ -171,19 +173,129 @@ func TestPinActions(t *testing.T) { } ]`)) + httpmock.RegisterResponder("GET", "https://api.github.com/repos/github/codeql-action/commits/v3", + httpmock.NewStringResponder(200, `d68b2d4edb4189fd2a5366ac14e72027bd4b37dd`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/github/codeql-action/git/matching-refs/tags/v3.", + httpmock.NewStringResponder(200, + `[ + { + "ref": "refs/tags/v3.28.2", + "object": { + "sha": "d68b2d4edb4189fd2a5366ac14e72027bd4b37dd", + "type": "commit" + } + } + ]`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/github/codeql-action/commits/v3.28.2", + httpmock.NewStringResponder(200, `d68b2d4edb4189fd2a5366ac14e72027bd4b37dd`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/github/codeql-action/git/matching-refs/tags/v3.28.2.", + httpmock.NewStringResponder(200, + `[ + { + "ref": "refs/tags/v3.28.2", + "object": { + "sha": "d68b2d4edb4189fd2a5366ac14e72027bd4b37dd", + "type": "commit" + } + } + ]`)) + + // mock ping response + httpmock.RegisterResponder("GET", "https://ghcr.io/v2/", + httpmock.NewStringResponder(200, ``)) + + // Mock token endpoints + httpmock.RegisterResponder("GET", "https://ghcr.io/token", + func(req *http.Request) (*http.Response, error) { + scope := req.URL.Query().Get("scope") + switch scope { + // Following are the ones which simulate the image existance in ghcr + case "repository:actions/checkout:pull", + "repository:step-security/wait-for-secrets:pull", + "repository:actions/setup-node:pull", + "repository:peter-evans/close-issue:pull", + "repository:borales/actions-yarn:pull", + "repository:JS-DevTools/npm-publish:pull", + "repository:elgohr/Publish-Docker-Github-Action:pull", + "repository:brandedoutcast/publish-nuget:pull", + "repository:rohith/publish-nuget:pull", + "repository:github/codeql-action:pull": + return httpmock.NewJsonResponse(http.StatusOK, map[string]string{ + "token": "test-token", + "access_token": "test-token", + }) + default: + return httpmock.NewJsonResponse(http.StatusForbidden, map[string]interface{}{ + "errors": []map[string]string{ + { + "code": "DENIED", + "message": "requested access to the resource is denied", + }, + }, + }) + } + }) + + // Mock manifest endpoints for specific versions and commit hashes + manifestResponders := []string{ + // the following list will contain the list of actions with versions + // which are mocked to be immutable + "actions/checkout@v1.2.0", + "github/codeql-action@v3.28.2", + } + + for _, action := range manifestResponders { + actionPath := strings.Split(action, "@")[0] + version := strings.TrimPrefix(strings.Split(action, "@")[1], "v") + // Mock manifest response so that we can treat action as immutable + httpmock.RegisterResponder("GET", "https://ghcr.io/v2/"+actionPath+"/manifests/"+version, + func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(http.StatusOK, map[string]interface{}{ + "schemaVersion": 2, + "mediaType": "application/vnd.github.actions.package.v1+json", + "artifactType": "application/vnd.github.actions.package.v1+json", + "config": map[string]interface{}{ + "mediaType": "application/vnd.github.actions.package.v1+json", + }, + }) + }) + } + + // Default manifest response for non-existent tags + httpmock.RegisterResponder("GET", `=~^https://ghcr\.io/v2/.*/manifests/.*`, + func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(http.StatusNotFound, map[string]interface{}{ + "errors": []map[string]string{ + { + "code": "MANIFEST_UNKNOWN", + "message": "manifest unknown", + }, + }, + }) + }) + tests := []struct { - fileName string - wantUpdated bool + fileName string + wantUpdated bool + exemptedActions []string + pinToImmutable bool }{ - {fileName: "alreadypinned.yml", wantUpdated: false}, - {fileName: "branch.yml", wantUpdated: true}, - {fileName: "localaction.yml", wantUpdated: true}, - {fileName: "multiplejobs.yml", wantUpdated: true}, - {fileName: "basic.yml", wantUpdated: true}, - {fileName: "dockeraction.yml", wantUpdated: true}, - {fileName: "multipleactions.yml", wantUpdated: true}, - {fileName: "actionwithcomment.yml", wantUpdated: true}, - {fileName: "repeatedactionwithcomment.yml", wantUpdated: true}, + {fileName: "alreadypinned.yml", wantUpdated: false, pinToImmutable: true}, + {fileName: "branch.yml", wantUpdated: true, pinToImmutable: true}, + {fileName: "localaction.yml", wantUpdated: true, pinToImmutable: true}, + {fileName: "multiplejobs.yml", wantUpdated: true, pinToImmutable: true}, + {fileName: "basic.yml", wantUpdated: true, pinToImmutable: true}, + {fileName: "dockeraction.yml", wantUpdated: true, pinToImmutable: true}, + {fileName: "multipleactions.yml", wantUpdated: true, pinToImmutable: true}, + {fileName: "actionwithcomment.yml", wantUpdated: true, pinToImmutable: true}, + {fileName: "repeatedactionwithcomment.yml", wantUpdated: true, pinToImmutable: true}, + {fileName: "immutableaction-1.yml", wantUpdated: true, pinToImmutable: true}, + {fileName: "exemptaction.yml", wantUpdated: true, exemptedActions: []string{"actions/checkout", "rohith/*"}, pinToImmutable: true}, + {fileName: "donotpintoimmutable.yml", wantUpdated: true, pinToImmutable: false}, + {fileName: "invertedcommas.yml", wantUpdated: true, pinToImmutable: false}, } for _, tt := range tests { input, err := ioutil.ReadFile(path.Join(inputDirectory, tt.fileName)) @@ -192,7 +304,7 @@ func TestPinActions(t *testing.T) { log.Fatal(err) } - output, gotUpdated, err := PinActions(string(input)) + output, gotUpdated, err := PinActions(string(input), tt.exemptedActions, tt.pinToImmutable) if tt.wantUpdated != gotUpdated { t.Errorf("test failed wantUpdated %v did not match gotUpdated %v", tt.wantUpdated, gotUpdated) } diff --git a/remediation/workflow/secureworkflow.go b/remediation/workflow/secureworkflow.go index 06cada8d2..44c1c07a8 100644 --- a/remediation/workflow/secureworkflow.go +++ b/remediation/workflow/secureworkflow.go @@ -1,8 +1,12 @@ package workflow import ( + "encoding/json" + "log" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" "github.com/step-security/secure-repo/remediation/workflow/hardenrunner" + "github.com/step-security/secure-repo/remediation/workflow/maintainedactions" "github.com/step-security/secure-repo/remediation/workflow/permissions" "github.com/step-security/secure-repo/remediation/workflow/pin" ) @@ -13,10 +17,30 @@ const ( HardenRunnerActionName = "Harden Runner" ) -func SecureWorkflow(queryStringParams map[string]string, inputYaml string, svc dynamodbiface.DynamoDBAPI) (*permissions.SecureWorkflowReponse, error) { - pinActions, addHardenRunner, addPermissions, addProjectComment := true, true, true, true - pinnedActions, addedHardenRunner, addedPermissions := false, false, false +func SecureWorkflow(queryStringParams map[string]string, inputYaml string, svc dynamodbiface.DynamoDBAPI, params ...interface{}) (*permissions.SecureWorkflowReponse, error) { + pinActions, addHardenRunner, addPermissions, addProjectComment, replaceMaintainedActions := true, true, true, true, false + pinnedActions, addedHardenRunner, addedPermissions, replacedMaintainedActions := false, false, false, false ignoreMissingKBs := false + enableLogging := false + addEmptyTopLevelPermissions := false + skipHardenRunnerForContainers := false + exemptedActions, pinToImmutable, maintainedActionsMap := []string{}, false, map[string]string{} + + if len(params) > 0 { + if v, ok := params[0].([]string); ok { + exemptedActions = v + } + } + if len(params) > 1 { + if v, ok := params[1].(bool); ok { + pinToImmutable = v + } + } + if len(params) > 2 { + if v, ok := params[2].(map[string]string); ok { + maintainedActionsMap = v + } + } if queryStringParams["pinActions"] == "false" { pinActions = false @@ -38,17 +62,54 @@ func SecureWorkflow(queryStringParams map[string]string, inputYaml string, svc d addProjectComment = false } + if len(maintainedActionsMap) > 0 { + replaceMaintainedActions = true + } + + if queryStringParams["enableLogging"] == "true" { + enableLogging = true + } + + if queryStringParams["addEmptyTopLevelPermissions"] == "true" { + addEmptyTopLevelPermissions = true + } + + if queryStringParams["skipHardenRunnerForContainers"] == "true" { + skipHardenRunnerForContainers = true + } + + if enableLogging { + // Log query parameters + paramsJSON, _ := json.MarshalIndent(queryStringParams, "", " ") + log.Printf("SecureWorkflow called with query parameters: %s", paramsJSON) + + // Log input YAML (complete) + log.Printf("Input YAML: %s", inputYaml) + } + secureWorkflowReponse := &permissions.SecureWorkflowReponse{FinalOutput: inputYaml, OriginalInput: inputYaml} var err error if addPermissions { - secureWorkflowReponse, err = permissions.AddJobLevelPermissions(secureWorkflowReponse.FinalOutput) + if enableLogging { + log.Printf("Adding job level permissions") + } + secureWorkflowReponse, err = permissions.AddJobLevelPermissions(secureWorkflowReponse.FinalOutput, addEmptyTopLevelPermissions) secureWorkflowReponse.OriginalInput = inputYaml if err != nil { + if enableLogging { + log.Printf("Error adding job level permissions: %v", err) + } return nil, err } else { if !secureWorkflowReponse.HasErrors || permissions.ShouldAddWorkflowLevelPermissions(secureWorkflowReponse.JobErrors) { - secureWorkflowReponse.FinalOutput, err = permissions.AddWorkflowLevelPermissions(secureWorkflowReponse.FinalOutput, addProjectComment) + if enableLogging { + log.Printf("Adding workflow level permissions") + } + secureWorkflowReponse.FinalOutput, err = permissions.AddWorkflowLevelPermissions(secureWorkflowReponse.FinalOutput, addProjectComment, addEmptyTopLevelPermissions) if err != nil { + if enableLogging { + log.Printf("Error adding workflow level permissions: %v", err) + } secureWorkflowReponse.HasErrors = true } else { // reset the error @@ -58,6 +119,9 @@ func SecureWorkflow(queryStringParams map[string]string, inputYaml string, svc d } } if len(secureWorkflowReponse.MissingActions) > 0 && !ignoreMissingKBs { + if enableLogging { + log.Printf("Storing missing actions: %v", secureWorkflowReponse.MissingActions) + } StoreMissingActions(secureWorkflowReponse.MissingActions, svc) } } @@ -66,20 +130,58 @@ func SecureWorkflow(queryStringParams map[string]string, inputYaml string, svc d addedPermissions = !secureWorkflowReponse.HasErrors } + if replaceMaintainedActions { + secureWorkflowReponse.FinalOutput, replacedMaintainedActions, err = maintainedactions.ReplaceActions(secureWorkflowReponse.FinalOutput, maintainedActionsMap) + if err != nil { + log.Printf("Error replacing maintained actions: %v", err) + secureWorkflowReponse.HasErrors = true + } + } + if pinActions { + if enableLogging { + log.Printf("Pinning GitHub Actions") + } pinnedAction, pinnedDocker := false, false - secureWorkflowReponse.FinalOutput, pinnedAction, _ = pin.PinActions(secureWorkflowReponse.FinalOutput) + secureWorkflowReponse.FinalOutput, pinnedAction, _ = pin.PinActions(secureWorkflowReponse.FinalOutput, exemptedActions, pinToImmutable) secureWorkflowReponse.FinalOutput, pinnedDocker, _ = pin.PinDocker(secureWorkflowReponse.FinalOutput) pinnedActions = pinnedAction || pinnedDocker + if enableLogging { + log.Printf("Pinned actions: %v, Pinned docker: %v", pinnedAction, pinnedDocker) + } } if addHardenRunner { - secureWorkflowReponse.FinalOutput, addedHardenRunner, _ = hardenrunner.AddAction(secureWorkflowReponse.FinalOutput, HardenRunnerActionPathWithTag, pinActions) + if enableLogging { + log.Printf("Adding harden runner action") + } + // Always pin harden-runner unless exempted + pinHardenRunner := true + if pin.ActionExists(HardenRunnerActionPath, exemptedActions) { + pinHardenRunner = false + if enableLogging { + log.Printf("Harden runner action is exempted from pinning") + } + } + secureWorkflowReponse.FinalOutput, addedHardenRunner, _ = hardenrunner.AddAction(secureWorkflowReponse.FinalOutput, HardenRunnerActionPathWithTag, pinHardenRunner, pinToImmutable, skipHardenRunnerForContainers) + if enableLogging { + log.Printf("Added harden runner: %v", addedHardenRunner) + } } // Setting appropriate flags secureWorkflowReponse.PinnedActions = pinnedActions secureWorkflowReponse.AddedHardenRunner = addedHardenRunner secureWorkflowReponse.AddedPermissions = addedPermissions + secureWorkflowReponse.AddedMaintainedActions = replacedMaintainedActions + + if enableLogging { + log.Printf("SecureWorkflow complete - PinnedActions: %v, AddedHardenRunner: %v, AddedPermissions: %v, HasErrors: %v", + secureWorkflowReponse.PinnedActions, + secureWorkflowReponse.AddedHardenRunner, + secureWorkflowReponse.AddedPermissions, + secureWorkflowReponse.HasErrors) + } + return secureWorkflowReponse, nil } diff --git a/remediation/workflow/secureworkflow_test.go b/remediation/workflow/secureworkflow_test.go index 9b5baa8b7..da64d9de6 100644 --- a/remediation/workflow/secureworkflow_test.go +++ b/remediation/workflow/secureworkflow_test.go @@ -8,6 +8,8 @@ import ( "testing" "github.com/jarcoal/httpmock" + "github.com/step-security/secure-repo/remediation/workflow/maintainedactions" + "github.com/step-security/secure-repo/remediation/workflow/permissions" ) func TestSecureWorkflow(t *testing.T) { @@ -107,12 +109,113 @@ func TestSecureWorkflow(t *testing.T) { ]`), ) + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/action-semantic-pull-request/releases/latest", + httpmock.NewStringResponder(200, `{ + "tag_name": "v5.5.5", + "name": "v5.5.5", + "body": "Release notes", + "created_at": "2023-01-01T00:00:00Z" + }`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/git-restore-mtime-action/releases/latest", + httpmock.NewStringResponder(200, `{ + "tag_name": "v2.1.0", + "name": "v2.1.0", + "body": "Release notes", + "created_at": "2023-01-01T00:00:00Z" + }`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/github/super-linter/releases/latest", + httpmock.NewStringResponder(200, `{ + "tag_name": "v4.9.0", + "name": "v4.9.0", + "body": "Release notes", + "created_at": "2023-01-01T00:00:00Z" + }`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/skip-duplicate-actions/releases/latest", + httpmock.NewStringResponder(200, `{ + "tag_name": "v2.1.0", + "name": "v2.1.0", + "body": "Release notes", + "created_at": "2023-01-01T00:00:00Z" + }`)) + + // Mock APIs for step-security/action-semantic-pull-request + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/action-semantic-pull-request/commits/v5", + httpmock.NewStringResponder(200, `a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/action-semantic-pull-request/git/matching-refs/tags/v5.", + httpmock.NewStringResponder(200, `[ + { + "ref": "refs/tags/v5.5.5", + "object": { + "sha": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0", + "type": "commit" + } + } + ]`)) + + // Mock APIs for step-security/skip-duplicate-actions + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/skip-duplicate-actions/commits/v2", + httpmock.NewStringResponder(200, `b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0a1`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/skip-duplicate-actions/git/matching-refs/tags/v2.", + httpmock.NewStringResponder(200, `[ + { + "ref": "refs/tags/v2.1.0", + "object": { + "sha": "b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0a1", + "type": "commit" + } + } + ]`)) + + // Mock APIs for step-security/git-restore-mtime-action + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/git-restore-mtime-action/commits/v2", + httpmock.NewStringResponder(200, `c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0a1b2`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/git-restore-mtime-action/git/matching-refs/tags/v2.", + httpmock.NewStringResponder(200, `[ + { + "ref": "refs/tags/v2.1.0", + "object": { + "sha": "c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0a1b2", + "type": "commit" + } + } + ]`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/actions-cache/commits/v1", + httpmock.NewStringResponder(200, `d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0a1b2c3`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/actions-cache/git/matching-refs/tags/v1.", + httpmock.NewStringResponder(200, `[ + { + "ref": "refs/tags/v1.0.0", + "object": { + "sha": "d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0a1b2c3", + "type": "commit" + } + } + ]`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/actions-cache/releases/latest", + httpmock.NewStringResponder(200, `{ + "tag_name": "v1.0.0", + "name": "v1.0.0", + "body": "Release notes", + "created_at": "2023-01-01T00:00:00Z" + }`)) + tests := []struct { - fileName string - wantPinnedActions bool - wantAddedHardenRunner bool - wantAddedPermissions bool + fileName string + wantPinnedActions bool + wantAddedHardenRunner bool + wantAddedPermissions bool + wantAddedMaintainedActions bool }{ + {fileName: "replaceactions.yml", wantPinnedActions: true, wantAddedHardenRunner: true, wantAddedPermissions: false, wantAddedMaintainedActions: true}, {fileName: "allscenarios.yml", wantPinnedActions: true, wantAddedHardenRunner: true, wantAddedPermissions: true}, {fileName: "missingaction.yml", wantPinnedActions: true, wantAddedHardenRunner: true, wantAddedPermissions: false}, {fileName: "nohardenrunner.yml", wantPinnedActions: true, wantAddedHardenRunner: false, wantAddedPermissions: true}, @@ -123,7 +226,9 @@ func TestSecureWorkflow(t *testing.T) { {fileName: "error.yml", wantPinnedActions: false, wantAddedHardenRunner: false, wantAddedPermissions: false}, } for _, test := range tests { - input, err := ioutil.ReadFile(path.Join(inputDirectory, test.fileName)) + var err error + var input []byte + input, err = ioutil.ReadFile(path.Join(inputDirectory, test.fileName)) if err != nil { log.Fatal(err) @@ -145,10 +250,25 @@ func TestSecureWorkflow(t *testing.T) { case "multiplejobperms.yml": queryParams["addHardenRunner"] = "false" queryParams["pinActions"] = "false" + case "replaceactions.yml": + queryParams["addMaintainedActions"] = "true" + queryParams["addHardenRunner"] = "true" + queryParams["pinActions"] = "true" + queryParams["addPermissions"] = "false" } queryParams["addProjectComment"] = "false" - output, err := SecureWorkflow(queryParams, string(input), &mockDynamoDBClient{}) + var output *permissions.SecureWorkflowReponse + var actionMap map[string]string + if test.fileName == "replaceactions.yml" { + actionMap, err = maintainedactions.LoadMaintainedActions("maintainedactions/maintainedActions.json") + if err != nil { + t.Errorf("unable to load the file %s", err) + } + output, err = SecureWorkflow(queryParams, string(input), &mockDynamoDBClient{}, []string{}, false, actionMap) + } else { + output, err = SecureWorkflow(queryParams, string(input), &mockDynamoDBClient{}) + } if err != nil { t.Errorf("Error not expected") @@ -175,6 +295,199 @@ func TestSecureWorkflow(t *testing.T) { if output.PinnedActions != test.wantPinnedActions { t.Errorf("test failed %s did not match expected PinnedActions value. Expected:%v Actual:%v", test.fileName, test.wantPinnedActions, output.PinnedActions) } + + if output.AddedMaintainedActions != test.wantAddedMaintainedActions { + t.Errorf("test failed %s did not match expected AddedMaintainedActions value. Expected:%v Actual:%v", test.fileName, test.wantAddedMaintainedActions, output.AddedMaintainedActions) + } + + } +} + +func TestSecureWorkflowContainerJob(t *testing.T) { + const inputDirectory = "../../testfiles/secureworkflow/input" + const outputDirectory = "../../testfiles/secureworkflow/output" + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + // Mock APIs for actions/checkout + httpmock.RegisterResponder("GET", "https://api.github.com/repos/actions/checkout/commits/v3", + httpmock.NewStringResponder(200, `c85c95e3d7251135ab7dc9ce3241c5835cc595a9`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/actions/checkout/git/matching-refs/tags/v3.", + httpmock.NewStringResponder(200, + `[ + { + "ref": "refs/tags/v3.5.3", + "object": { + "sha": "c85c95e3d7251135ab7dc9ce3241c5835cc595a9", + "type": "commit" + } + } + ]`), + ) + + // Mock APIs for step-security/harden-runner + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/harden-runner/commits/v2", + httpmock.NewStringResponder(200, `17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/harden-runner/git/matching-refs/tags/v2.", + httpmock.NewStringResponder(200, + `[ + { + "ref": "refs/tags/v2.8.1", + "object": { + "sha": "17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6", + "type": "commit" + } + } + ]`), + ) + + var err error + var input []byte + input, err = ioutil.ReadFile(path.Join(inputDirectory, "container-job.yml")) + + if err != nil { + log.Fatal(err) + } + + os.Setenv("KBFolder", "../../knowledge-base/actions") + + // Test with skipHardenRunnerForContainers = true + queryParams := make(map[string]string) + queryParams["skipHardenRunnerForContainers"] = "true" + queryParams["addProjectComment"] = "false" + + output, err := SecureWorkflow(queryParams, string(input), &mockDynamoDBClient{}) + + if err != nil { + t.Errorf("Error not expected") + } + + // Verify that harden runner was not added + if output.AddedHardenRunner { + t.Errorf("Harden runner should not be added for container job with skipHardenRunnerForContainers=true") + } + + // Verify that the output matches expected output file + expectedOutput, err := ioutil.ReadFile(path.Join(outputDirectory, "container-job.yml")) + if err != nil { + log.Fatal(err) + } + + if output.FinalOutput != string(expectedOutput) { + t.Errorf("test failed container-job.yml did not match expected output\nExpected:\n%s\n\nGot:\n%s", + string(expectedOutput), output.FinalOutput) + } + + // Verify permissions were added + if !output.AddedPermissions { + t.Errorf("Permissions should be added even for container jobs") + } + + // Verify actions were pinned + if !output.PinnedActions { + t.Errorf("Actions should be pinned even for container jobs") + } +} + +func TestSecureWorkflowEmptyPermissions(t *testing.T) { + const inputDirectory = "../../testfiles/secureworkflow/input" + const outputDirectory = "../../testfiles/secureworkflow/output" + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + // Mock APIs for actions/checkout + httpmock.RegisterResponder("GET", "https://api.github.com/repos/actions/checkout/commits/v2", + httpmock.NewStringResponder(200, `ee0669bd1cc54295c223e0bb666b733df41de1c5`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/actions/checkout/git/matching-refs/tags/v2.", + httpmock.NewStringResponder(200, + `[ + { + "ref": "refs/tags/v2.7.0", + "object": { + "sha": "ee0669bd1cc54295c223e0bb666b733df41de1c5", + "type": "commit" + } + } + ]`), + ) + + // Mock APIs for actions/setup-node + httpmock.RegisterResponder("GET", "https://api.github.com/repos/actions/setup-node/commits/v1", + httpmock.NewStringResponder(200, `f1f314fca9dfce2769ece7d933488f076716723e`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/actions/setup-node/git/matching-refs/tags/v1.", + httpmock.NewStringResponder(200, + `[ + { + "ref": "refs/tags/v1.4.6", + "object": { + "sha": "f1f314fca9dfce2769ece7d933488f076716723e", + "type": "commit" + } + } + ]`), + ) + + // Mock APIs for step-security/harden-runner + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/harden-runner/commits/v2", + httpmock.NewStringResponder(200, `17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6`)) + + httpmock.RegisterResponder("GET", "https://api.github.com/repos/step-security/harden-runner/git/matching-refs/tags/v2.", + httpmock.NewStringResponder(200, + `[ + { + "ref": "refs/tags/v2.8.1", + "object": { + "sha": "17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6", + "type": "commit" + } + } + ]`), + ) + + var err error + var input []byte + input, err = ioutil.ReadFile(path.Join(inputDirectory, "empty-permissions.yml")) + + if err != nil { + log.Fatal(err) + } + + os.Setenv("KBFolder", "../../knowledge-base/actions") + + queryParams := make(map[string]string) + queryParams["addEmptyTopLevelPermissions"] = "true" + queryParams["addProjectComment"] = "false" + + output, err := SecureWorkflow(queryParams, string(input), &mockDynamoDBClient{}) + + if err != nil { + t.Errorf("Error not expected") + } + + expectedOutput, err := ioutil.ReadFile(path.Join(outputDirectory, "empty-permissions.yml")) + + if err != nil { + log.Fatal(err) + } + + if output.FinalOutput != string(expectedOutput) { + // Write the actual output to a file for debugging + debugFile := path.Join(outputDirectory, "empty-permissions-debug.yml") + err := ioutil.WriteFile(debugFile, []byte(output.FinalOutput), 0644) + if err != nil { + t.Logf("Failed to write debug file: %v", err) + } else { + t.Logf("Actual output written to: %s", debugFile) + } + + t.Errorf("test failed empty-permissions.yml did not match expected output\nExpected:\n%s\n\nGot:\n%s", + string(expectedOutput), output.FinalOutput) } } diff --git a/testfiles/addaction/input/container-job.yml b/testfiles/addaction/input/container-job.yml new file mode 100644 index 000000000..e13bcc714 --- /dev/null +++ b/testfiles/addaction/input/container-job.yml @@ -0,0 +1,18 @@ +name: "Container job workflow" + +on: + push: + + +jobs: + test: + runs-on: ubuntu-latest + container: + image: cgr.dev/chainguard/wolfi-base@sha256:91ed94ec4e72368a9b5113f2ffb1d8e783a91db489011a89d9fad3e3816a75ba + options: >- + --health-cmd pg_isready + --health-interval 10s + + steps: + - name: Checkout + uses: actions/checkout@v3 \ No newline at end of file diff --git a/testfiles/addaction/input/jobNameInInput.yml b/testfiles/addaction/input/jobNameInInput.yml new file mode 100644 index 000000000..afa6c5a1d --- /dev/null +++ b/testfiles/addaction/input/jobNameInInput.yml @@ -0,0 +1,65 @@ +name: coveo-example-library + +on: + push: + branches: + - main + paths: + - 'coveo-example-library/**' + - '!**.lock' + - '!**.md' + + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'coveo-example-library/**' + - '.github/workflows/coveo-example-library.yml' + - '!**.md' + + workflow_dispatch: + inputs: + publish: + description: "Publish to pypi.org?" + required: false + default: 'false' + +jobs: + pyprojectci: + name: pyproject ci + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.10"] + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - name: Run stew ci + uses: coveo/stew@main + with: + project-name: ${{ github.workflow }} + python-version: ${{ matrix.python-version }} + poetry-version: "<2" + + publish: + name: Publish to pypi.org + runs-on: ubuntu-20.04 + needs: pyprojectci + + steps: + - name: Checkout repository + uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 + + - name: Setup python 3.9 + uses: actions/setup-python@e9aba2c848f5ebd159c070c61ea2c4e2b122355e # v2.3.4 + with: + python-version: 3.9 + + - name: Publish to pypi + uses: ./.github/workflows/actions/publish-to-pypi + with: + project-name: ${{ github.workflow }} + pypi-token: ${{ secrets.PYPI_TOKEN_COVEO_EXAMPLE_LIBRARY }} + pre-release: ${{ github.ref != 'refs/heads/main' }} + dry-run: true \ No newline at end of file diff --git a/testfiles/addaction/output/2jobs.yml b/testfiles/addaction/output/2jobs.yml index ce942a75f..3539b2c23 100644 --- a/testfiles/addaction/output/2jobs.yml +++ b/testfiles/addaction/output/2jobs.yml @@ -5,7 +5,7 @@ jobs: list-directory: runs-on: ubuntu-latest steps: - - name: Harden Runner + - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@v2 with: egress-policy: audit @@ -14,7 +14,7 @@ jobs: list-directory1: runs-on: ubuntu-latest steps: - - name: Harden Runner + - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@v2 with: egress-policy: audit diff --git a/testfiles/addaction/output/action-issues.yml b/testfiles/addaction/output/action-issues.yml index 0596710d1..745eabff8 100644 --- a/testfiles/addaction/output/action-issues.yml +++ b/testfiles/addaction/output/action-issues.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: Harden Runner + - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@v2 with: egress-policy: audit diff --git a/testfiles/addaction/output/alreadypresent.yml b/testfiles/addaction/output/alreadypresent.yml index d82449ae3..50d0797a7 100644 --- a/testfiles/addaction/output/alreadypresent.yml +++ b/testfiles/addaction/output/alreadypresent.yml @@ -10,7 +10,7 @@ jobs: list-directory1: runs-on: ubuntu-latest steps: - - name: Harden Runner + - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@v2 with: egress-policy: audit diff --git a/testfiles/addaction/output/container-job.yml b/testfiles/addaction/output/container-job.yml new file mode 100644 index 000000000..e13bcc714 --- /dev/null +++ b/testfiles/addaction/output/container-job.yml @@ -0,0 +1,18 @@ +name: "Container job workflow" + +on: + push: + + +jobs: + test: + runs-on: ubuntu-latest + container: + image: cgr.dev/chainguard/wolfi-base@sha256:91ed94ec4e72368a9b5113f2ffb1d8e783a91db489011a89d9fad3e3816a75ba + options: >- + --health-cmd pg_isready + --health-interval 10s + + steps: + - name: Checkout + uses: actions/checkout@v3 \ No newline at end of file diff --git a/testfiles/addaction/output/jobNameInInput.yml b/testfiles/addaction/output/jobNameInInput.yml new file mode 100644 index 000000000..413336ce8 --- /dev/null +++ b/testfiles/addaction/output/jobNameInInput.yml @@ -0,0 +1,75 @@ +name: coveo-example-library + +on: + push: + branches: + - main + paths: + - 'coveo-example-library/**' + - '!**.lock' + - '!**.md' + + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'coveo-example-library/**' + - '.github/workflows/coveo-example-library.yml' + - '!**.md' + + workflow_dispatch: + inputs: + publish: + description: "Publish to pypi.org?" + required: false + default: 'false' + +jobs: + pyprojectci: + name: pyproject ci + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.10"] + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Run stew ci + uses: coveo/stew@main + with: + project-name: ${{ github.workflow }} + python-version: ${{ matrix.python-version }} + poetry-version: "<2" + + publish: + name: Publish to pypi.org + runs-on: ubuntu-20.04 + needs: pyprojectci + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 + + - name: Setup python 3.9 + uses: actions/setup-python@e9aba2c848f5ebd159c070c61ea2c4e2b122355e # v2.3.4 + with: + python-version: 3.9 + + - name: Publish to pypi + uses: ./.github/workflows/actions/publish-to-pypi + with: + project-name: ${{ github.workflow }} + pypi-token: ${{ secrets.PYPI_TOKEN_COVEO_EXAMPLE_LIBRARY }} + pre-release: ${{ github.ref != 'refs/heads/main' }} + dry-run: true \ No newline at end of file diff --git a/testfiles/addworkflow/expected-codeql.yml b/testfiles/addworkflow/expected-codeql.yml index ad8b02e8c..3f2fea5c9 100644 --- a/testfiles/addworkflow/expected-codeql.yml +++ b/testfiles/addworkflow/expected-codeql.yml @@ -41,11 +41,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -55,7 +55,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -68,6 +68,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/testfiles/addworkflow/expected-dependency-review.yml b/testfiles/addworkflow/expected-dependency-review.yml index bd4d79445..d119b46dd 100644 --- a/testfiles/addworkflow/expected-dependency-review.yml +++ b/testfiles/addworkflow/expected-dependency-review.yml @@ -17,6 +17,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: 'Dependency Review' - uses: actions/dependency-review-action@v2 + uses: actions/dependency-review-action@v4 diff --git a/testfiles/addworkflow/expected-scorecards.yml b/testfiles/addworkflow/expected-scorecards.yml index 5ce6cef6c..6c3dc6436 100644 --- a/testfiles/addworkflow/expected-scorecards.yml +++ b/testfiles/addworkflow/expected-scorecards.yml @@ -28,15 +28,20 @@ jobs: id-token: write contents: read actions: read + # To allow GraphQL ListCommits to work + issues: read + pull-requests: read + # To detect SAST tools + checks: read steps: - name: "Checkout code" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # v2.0.6 + uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 with: results_file: results.sarif results_format: sarif @@ -58,7 +63,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: SARIF file path: results.sarif @@ -66,6 +71,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: results.sarif diff --git a/testfiles/dependabotfiles/input/extra-slash.yml b/testfiles/dependabotfiles/input/extra-slash.yml new file mode 100644 index 000000000..5ab1c551d --- /dev/null +++ b/testfiles/dependabotfiles/input/extra-slash.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "npm" + # Files stored in `app` directory + directory: "/sample/" + schedule: + interval: "daily" \ No newline at end of file diff --git a/testfiles/dependabotfiles/output/extra-slash.yml b/testfiles/dependabotfiles/output/extra-slash.yml new file mode 100644 index 000000000..6f65f32e6 --- /dev/null +++ b/testfiles/dependabotfiles/output/extra-slash.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "npm" + # Files stored in `app` directory + directory: "/sample/" + schedule: + interval: "daily" diff --git a/testfiles/joblevelpermskb/input/empty-top-level-permissions.yml b/testfiles/joblevelpermskb/input/empty-top-level-permissions.yml new file mode 100644 index 000000000..6ea984f89 --- /dev/null +++ b/testfiles/joblevelpermskb/input/empty-top-level-permissions.yml @@ -0,0 +1,13 @@ +name: "checkout only workflow" + +on: + push: + + +jobs: + checkout-job: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 \ No newline at end of file diff --git a/testfiles/joblevelpermskb/input/github-token-in-job-env.yml b/testfiles/joblevelpermskb/input/github-token-in-job-env.yml new file mode 100644 index 000000000..a1368dcb3 --- /dev/null +++ b/testfiles/joblevelpermskb/input/github-token-in-job-env.yml @@ -0,0 +1,16 @@ +name: Job level env +on: + pull_request: + branches: [main] + +jobs: + job-with-error: + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + - name: some step that uses token + run: | + npm ci \ No newline at end of file diff --git a/testfiles/joblevelpermskb/output/empty-top-level-permissions.yml b/testfiles/joblevelpermskb/output/empty-top-level-permissions.yml new file mode 100644 index 000000000..2febb7397 --- /dev/null +++ b/testfiles/joblevelpermskb/output/empty-top-level-permissions.yml @@ -0,0 +1,15 @@ +name: "checkout only workflow" + +on: + push: + + +jobs: + checkout-job: + permissions: + contents: read # for actions/checkout to fetch code + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 \ No newline at end of file diff --git a/testfiles/joblevelpermskb/output/github-token-in-job-env.yml b/testfiles/joblevelpermskb/output/github-token-in-job-env.yml new file mode 100644 index 000000000..19a913d73 --- /dev/null +++ b/testfiles/joblevelpermskb/output/github-token-in-job-env.yml @@ -0,0 +1 @@ +KnownIssue-8: Permissions were not added to the jobs since it has GITHUB_TOKEN in job level env variable \ No newline at end of file diff --git a/testfiles/maintainedActions/input/doubleJob.yml b/testfiles/maintainedActions/input/doubleJob.yml new file mode 100644 index 000000000..9b4807e7c --- /dev/null +++ b/testfiles/maintainedActions/input/doubleJob.yml @@ -0,0 +1,31 @@ +name: Test Workflow - Double Job +on: push + +jobs: + test: + runs-on: ubuntu-latest + 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: step-security/git-restore-mtime-action@v2 + with: + pattern: '**/*' + + build: + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '16' + - uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- \ No newline at end of file diff --git a/testfiles/maintainedActions/input/exemtedMaintainedActions.yml b/testfiles/maintainedActions/input/exemtedMaintainedActions.yml new file mode 100644 index 000000000..40dedd4b7 --- /dev/null +++ b/testfiles/maintainedActions/input/exemtedMaintainedActions.yml @@ -0,0 +1,35 @@ +name: Test Workflow +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - 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: '**/*' + + build: + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '16' + - uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - uses: amannn/action-semantic-pull-request@v5 + with: + types: feat,fix,chore \ No newline at end of file diff --git a/testfiles/maintainedActions/input/noChangesNeeded.yml b/testfiles/maintainedActions/input/noChangesNeeded.yml new file mode 100644 index 000000000..5557b01e6 --- /dev/null +++ b/testfiles/maintainedActions/input/noChangesNeeded.yml @@ -0,0 +1,17 @@ +name: Test Workflow - No Changes Needed +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: step-security/checkout@v3 + - uses: step-security/action-semantic-pull-request@v5.5.5 + with: + types: feat,fix,chore + - uses: step-security/skip-duplicate-actions@v5.3.2 + with: + do_not_skip: '["release"]' + - uses: step-security/git-restore-mtime-action@v2.1.0 + with: + pattern: '**/*' \ No newline at end of file diff --git a/testfiles/maintainedActions/input/oneJob.yml b/testfiles/maintainedActions/input/oneJob.yml new file mode 100644 index 000000000..939d87c02 --- /dev/null +++ b/testfiles/maintainedActions/input/oneJob.yml @@ -0,0 +1,23 @@ +name: Test Workflow +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - 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: '**/*' + - uses: tespkg/actions-cache/restore@v1 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- \ No newline at end of file diff --git a/testfiles/maintainedActions/output/doubleJob.yml b/testfiles/maintainedActions/output/doubleJob.yml new file mode 100644 index 000000000..703b8c969 --- /dev/null +++ b/testfiles/maintainedActions/output/doubleJob.yml @@ -0,0 +1,31 @@ +name: Test Workflow - Double Job +on: push + +jobs: + test: + runs-on: ubuntu-latest + 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: '**/*' + + build: + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '16' + - uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- \ No newline at end of file diff --git a/testfiles/maintainedActions/output/exemtedMaintainedActions.yml b/testfiles/maintainedActions/output/exemtedMaintainedActions.yml new file mode 100644 index 000000000..f652f3945 --- /dev/null +++ b/testfiles/maintainedActions/output/exemtedMaintainedActions.yml @@ -0,0 +1,35 @@ +name: Test Workflow +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - 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: chetan/git-restore-mtime-action@v1 + with: + pattern: '**/*' + + build: + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '16' + - uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - uses: step-security/action-semantic-pull-request@v5 + with: + types: feat,fix,chore \ No newline at end of file diff --git a/testfiles/maintainedActions/output/noChangesNeeded.yml b/testfiles/maintainedActions/output/noChangesNeeded.yml new file mode 100644 index 000000000..5557b01e6 --- /dev/null +++ b/testfiles/maintainedActions/output/noChangesNeeded.yml @@ -0,0 +1,17 @@ +name: Test Workflow - No Changes Needed +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: step-security/checkout@v3 + - uses: step-security/action-semantic-pull-request@v5.5.5 + with: + types: feat,fix,chore + - uses: step-security/skip-duplicate-actions@v5.3.2 + with: + do_not_skip: '["release"]' + - uses: step-security/git-restore-mtime-action@v2.1.0 + with: + pattern: '**/*' \ No newline at end of file diff --git a/testfiles/maintainedActions/output/oneJob.yml b/testfiles/maintainedActions/output/oneJob.yml new file mode 100644 index 000000000..e3ed9c165 --- /dev/null +++ b/testfiles/maintainedActions/output/oneJob.yml @@ -0,0 +1,23 @@ +name: Test Workflow +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - 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: '**/*' + - uses: step-security/actions-cache/restore@v1 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- \ No newline at end of file diff --git a/testfiles/pinactions/immutableActionResponses/actions-checkout-manifests-4.2.2.json b/testfiles/pinactions/immutableActionResponses/actions-checkout-manifests-4.2.2.json new file mode 100644 index 000000000..d93e66789 --- /dev/null +++ b/testfiles/pinactions/immutableActionResponses/actions-checkout-manifests-4.2.2.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "artifactType": "application/vnd.github.actions.package.v1+json", + "config": { + "mediaType": "application/vnd.oci.empty.v1+json", + "size": 2, + "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a" + }, + "layers": [ + { + "mediaType": "application/vnd.github.actions.package.layer.v1.tar+gzip", + "size": 424913, + "digest": "sha256:309d5e1a7604a5e688e2b55a6763e4109eb07349dca6d3c44e85c57ab2bb4f3f", + "annotations": { + "org.opencontainers.image.title": "actions-checkout_4.2.2.tar.gz" + } + }, + { + "mediaType": "application/vnd.github.actions.package.layer.v1.zip", + "size": 546845, + "digest": "sha256:e9808fe811a75b46234757f9566987635166bca838090fcbc8021a0d45c737b3", + "annotations": { + "org.opencontainers.image.title": "actions-checkout_4.2.2.zip" + } + } + ], + "annotations": { + "org.opencontainers.image.created": "2024-10-23T14:46:13.071Z", + "action.tar.gz.digest": "sha256:309d5e1a7604a5e688e2b55a6763e4109eb07349dca6d3c44e85c57ab2bb4f3f", + "action.zip.digest": "sha256:e9808fe811a75b46234757f9566987635166bca838090fcbc8021a0d45c737b3", + "com.github.package.type": "actions_oci_pkg", + "com.github.package.version": "4.2.2", + "com.github.source.repo.id": "197814629", + "com.github.source.repo.owner.id": "44036562", + "com.github.source.commit": "11bd71901bbe5b1630ceea73d27597364c9af683" + } +} \ No newline at end of file diff --git a/testfiles/pinactions/immutableActionResponses/default.json b/testfiles/pinactions/immutableActionResponses/default.json new file mode 100644 index 000000000..d8e11ba40 --- /dev/null +++ b/testfiles/pinactions/immutableActionResponses/default.json @@ -0,0 +1,8 @@ +{ + "errors":[ + { + "code":"MANIFEST_UNKNOWN", + "message":"manifest unknown" + } + ] +} \ No newline at end of file diff --git a/testfiles/pinactions/immutableActionResponses/step-security-wait-for-secrets-manifests-1.2.0.json b/testfiles/pinactions/immutableActionResponses/step-security-wait-for-secrets-manifests-1.2.0.json new file mode 100644 index 000000000..d85d31251 --- /dev/null +++ b/testfiles/pinactions/immutableActionResponses/step-security-wait-for-secrets-manifests-1.2.0.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "artifactType": "application/vnd.github.actions.package.v1+json", + "config": { + "mediaType": "application/vnd.oci.empty.v1+json", + "size": 2, + "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a" + }, + "layers": [ + { + "mediaType": "application/vnd.github.actions.package.layer.v1.tar+gzip", + "size": 689381, + "digest": "sha256:6390cea2d46095ef08dd2746d4323b11b7d1190d7e9ad9ef4a23b8ee5481d295", + "annotations": { + "org.opencontainers.image.title": "step-security-wait-for-secrets_1.2.0.tar.gz" + } + }, + { + "mediaType": "application/vnd.github.actions.package.layer.v1.zip", + "size": 723541, + "digest": "sha256:56f5004c2b1bff0f148c3998aa0f5bd47a315a602428031b8ba72d881edfb429", + "annotations": { + "org.opencontainers.image.title": "step-security-wait-for-secrets_1.2.0.zip" + } + } + ], + "annotations": { + "org.opencontainers.image.created": "2024-10-24T05:13:19.501Z", + "action.tar.gz.digest": "sha256:6390cea2d46095ef08dd2746d4323b11b7d1190d7e9ad9ef4a23b8ee5481d295", + "action.zip.digest": "sha256:56f5004c2b1bff0f148c3998aa0f5bd47a315a602428031b8ba72d881edfb429", + "com.github.package.type": "actions_oci_pkg", + "com.github.package.version": "1.2.0", + "com.github.source.repo.id": "498456330", + "com.github.source.repo.owner.id": "88700172", + "com.github.source.commit": "5809f7d044804a5a1d43217fa8f3e855939fc9ef" + } +} \ No newline at end of file diff --git a/testfiles/pinactions/input/donotpintoimmutable.yml b/testfiles/pinactions/input/donotpintoimmutable.yml new file mode 100644 index 000000000..922c6f8ef --- /dev/null +++ b/testfiles/pinactions/input/donotpintoimmutable.yml @@ -0,0 +1,12 @@ +name: Integration Test Github +on: [push] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: github/codeql-action/analyze@v3.28.2 + - uses: borales/actions-yarn@v2.3.0 + with: + auth-token: ${{ secrets.GITHUB_TOKEN }} + registry-url: npm.pkg.github.com diff --git a/testfiles/pinactions/input/exemptaction.yml b/testfiles/pinactions/input/exemptaction.yml new file mode 100644 index 000000000..3a80dc799 --- /dev/null +++ b/testfiles/pinactions/input/exemptaction.yml @@ -0,0 +1,44 @@ +name: publish to nuget +on: + push: + branches: + - master # Default release branch +jobs: + publish: + name: build, pack & publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + + # - name: Setup dotnet + # uses: actions/setup-dotnet@v1 + # with: + # dotnet-version: 3.1.200 + + # Publish + - name: publish on version change + id: publish_nuget + uses: brandedoutcast/publish-nuget@v2 + with: + PROJECT_FILE_PATH: Core/Core.csproj + NUGET_KEY: ${{ secrets.GITHUB_TOKEN }} + NUGET_SOURCE: https://nuget.pkg.github.com/OWNER/index.json + publish1: + name: build, pack & publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + + # - name: Setup dotnet + # uses: actions/setup-dotnet@v1 + # with: + # dotnet-version: 3.1.200 + + # Publish + - name: publish on version change + id: publish_nuget + uses: rohith/publish-nuget@v2 + with: + PROJECT_FILE_PATH: Core/Core.csproj + NUGET_KEY: ${{ secrets.GITHUB_TOKEN }} + NUGET_SOURCE: https://nuget.pkg.github.com/OWNER/index.json \ No newline at end of file diff --git a/testfiles/pinactions/input/immutableaction-1.yml b/testfiles/pinactions/input/immutableaction-1.yml new file mode 100644 index 000000000..3740d1637 --- /dev/null +++ b/testfiles/pinactions/input/immutableaction-1.yml @@ -0,0 +1,12 @@ +name: Integration Test Github +on: [push] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: github/codeql-action/analyze@v3 + - uses: borales/actions-yarn@v2.3.0 + with: + auth-token: ${{ secrets.GITHUB_TOKEN }} + registry-url: npm.pkg.github.com diff --git a/testfiles/pinactions/input/invertedcommas.yml b/testfiles/pinactions/input/invertedcommas.yml new file mode 100644 index 000000000..c4fce8311 --- /dev/null +++ b/testfiles/pinactions/input/invertedcommas.yml @@ -0,0 +1,15 @@ +name: "close issue" + +on: + push: + +jobs: + closeissue: + runs-on: ubuntu-latest + + steps: + - name: Close Issue + uses: "peter-evans/close-issue@v1" + with: + issue-number: 1 + comment: Auto-closing issue diff --git a/testfiles/pinactions/output/actionwithcomment.yml b/testfiles/pinactions/output/actionwithcomment.yml index a1131c832..89388bb10 100644 --- a/testfiles/pinactions/output/actionwithcomment.yml +++ b/testfiles/pinactions/output/actionwithcomment.yml @@ -16,7 +16,7 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9 # v1.2.0 + - uses: actions/checkout@v1.2.0 - uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e # v1.4.6 with: node-version: 10 diff --git a/testfiles/pinactions/output/dockeraction.yml b/testfiles/pinactions/output/dockeraction.yml index 6e3948640..1413f1e52 100644 --- a/testfiles/pinactions/output/dockeraction.yml +++ b/testfiles/pinactions/output/dockeraction.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9 # v1.2.0 + uses: actions/checkout@v1.2.0 - name: Integration test uses: docker://ghcr.io/step-security/integration-test/int:latest env: diff --git a/testfiles/pinactions/output/donotpintoimmutable.yml b/testfiles/pinactions/output/donotpintoimmutable.yml new file mode 100644 index 000000000..4bfaf3f66 --- /dev/null +++ b/testfiles/pinactions/output/donotpintoimmutable.yml @@ -0,0 +1,12 @@ +name: Integration Test Github +on: [push] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9 # v1.2.0 + - uses: github/codeql-action/analyze@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 + - uses: borales/actions-yarn@4965e1a0f0ae9c422a9a5748ebd1fb5e097d22b9 # v2.3.0 + with: + auth-token: ${{ secrets.GITHUB_TOKEN }} + registry-url: npm.pkg.github.com diff --git a/testfiles/pinactions/output/exemptaction.yml b/testfiles/pinactions/output/exemptaction.yml new file mode 100644 index 000000000..4c986d6fd --- /dev/null +++ b/testfiles/pinactions/output/exemptaction.yml @@ -0,0 +1,44 @@ +name: publish to nuget +on: + push: + branches: + - master # Default release branch +jobs: + publish: + name: build, pack & publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + + # - name: Setup dotnet + # uses: actions/setup-dotnet@v1 + # with: + # dotnet-version: 3.1.200 + + # Publish + - name: publish on version change + id: publish_nuget + uses: brandedoutcast/publish-nuget@c12b8546b67672ee38ac87bea491ac94a587f7cc # v2.5.5 + with: + PROJECT_FILE_PATH: Core/Core.csproj + NUGET_KEY: ${{ secrets.GITHUB_TOKEN }} + NUGET_SOURCE: https://nuget.pkg.github.com/OWNER/index.json + publish1: + name: build, pack & publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + + # - name: Setup dotnet + # uses: actions/setup-dotnet@v1 + # with: + # dotnet-version: 3.1.200 + + # Publish + - name: publish on version change + id: publish_nuget + uses: rohith/publish-nuget@v2 + with: + PROJECT_FILE_PATH: Core/Core.csproj + NUGET_KEY: ${{ secrets.GITHUB_TOKEN }} + NUGET_SOURCE: https://nuget.pkg.github.com/OWNER/index.json \ No newline at end of file diff --git a/testfiles/pinactions/output/immutableaction-1.yml b/testfiles/pinactions/output/immutableaction-1.yml new file mode 100644 index 000000000..b007a0e7d --- /dev/null +++ b/testfiles/pinactions/output/immutableaction-1.yml @@ -0,0 +1,12 @@ +name: Integration Test Github +on: [push] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1.2.0 + - uses: github/codeql-action/analyze@v3.28.2 + - uses: borales/actions-yarn@4965e1a0f0ae9c422a9a5748ebd1fb5e097d22b9 # v2.3.0 + with: + auth-token: ${{ secrets.GITHUB_TOKEN }} + registry-url: npm.pkg.github.com diff --git a/testfiles/pinactions/output/invertedcommas.yml b/testfiles/pinactions/output/invertedcommas.yml new file mode 100644 index 000000000..d5691aaf2 --- /dev/null +++ b/testfiles/pinactions/output/invertedcommas.yml @@ -0,0 +1,15 @@ +name: "close issue" + +on: + push: + +jobs: + closeissue: + runs-on: ubuntu-latest + + steps: + - name: Close Issue + uses: "peter-evans/close-issue@a700eac5bf2a1c7a8cb6da0c13f93ed96fd53dbe" # v1.0.3 + with: + issue-number: 1 + comment: Auto-closing issue diff --git a/testfiles/pinactions/output/localaction.yml b/testfiles/pinactions/output/localaction.yml index c6d98688b..f79109707 100644 --- a/testfiles/pinactions/output/localaction.yml +++ b/testfiles/pinactions/output/localaction.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e # v1.4.6 with: node-version: 12.x - - uses: actions/checkout@544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9 # v1.2.0 + - uses: actions/checkout@v1.2.0 - run: npm ci - run: npm run build - run: npm run format-check @@ -32,7 +32,7 @@ jobs: steps: # Clone this repo - name: Checkout - uses: actions/checkout@544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9 # v1.2.0 + uses: actions/checkout@v1.2.0 # Basic checkout - name: Checkout basic @@ -150,7 +150,7 @@ jobs: steps: # Clone this repo - name: Checkout - uses: actions/checkout@544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9 # v1.2.0 + uses: actions/checkout@v1.2.0 # Basic checkout using git - name: Checkout basic @@ -182,7 +182,7 @@ jobs: steps: # Clone this repo - name: Checkout - uses: actions/checkout@544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9 # v1.2.0 + uses: actions/checkout@v1.2.0 # Basic checkout using git - name: Checkout basic diff --git a/testfiles/pinactions/output/multipleactions.yml b/testfiles/pinactions/output/multipleactions.yml index 9b9abf9c9..1e4a76422 100644 --- a/testfiles/pinactions/output/multipleactions.yml +++ b/testfiles/pinactions/output/multipleactions.yml @@ -4,7 +4,7 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9 # v1.2.0 + - uses: actions/checkout@v1.2.0 - uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e # v1.4.6 with: node-version: 10 diff --git a/testfiles/pinactions/output/multiplejobs.yml b/testfiles/pinactions/output/multiplejobs.yml index 6d2d47211..435f8b621 100644 --- a/testfiles/pinactions/output/multiplejobs.yml +++ b/testfiles/pinactions/output/multiplejobs.yml @@ -8,7 +8,7 @@ jobs: name: build, pack & publish runs-on: ubuntu-latest steps: - - uses: actions/checkout@544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9 # v1.2.0 + - uses: actions/checkout@v1.2.0 # - name: Setup dotnet # uses: actions/setup-dotnet@v1 @@ -27,7 +27,7 @@ jobs: name: build, pack & publish runs-on: ubuntu-latest steps: - - uses: actions/checkout@544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9 # v1.2.0 + - uses: actions/checkout@v1.2.0 # - name: Setup dotnet # uses: actions/setup-dotnet@v1 diff --git a/testfiles/pinactions/output/repeatedactionwithcomment.yml b/testfiles/pinactions/output/repeatedactionwithcomment.yml index dbd50839e..0cfd8a115 100644 --- a/testfiles/pinactions/output/repeatedactionwithcomment.yml +++ b/testfiles/pinactions/output/repeatedactionwithcomment.yml @@ -16,7 +16,7 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9 # v1.2.0 + - uses: actions/checkout@v1.2.0 - uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e # v1.4.6 with: node-version: 10 diff --git a/testfiles/secureworkflow/input/container-job.yml b/testfiles/secureworkflow/input/container-job.yml new file mode 100644 index 000000000..29bcc0f0b --- /dev/null +++ b/testfiles/secureworkflow/input/container-job.yml @@ -0,0 +1,20 @@ +name: "Container job workflow" + +on: + push: + + +jobs: + test: + runs-on: ubuntu-latest + container: + image: cgr.dev/chainguard/wolfi-base@sha256:91ed94ec4e72368a9b5113f2ffb1d8e783a91db489011a89d9fad3e3816a75ba + options: >- + --health-cmd pg_isready + --health-interval 10s + + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Run tests + run: npm test \ No newline at end of file diff --git a/testfiles/secureworkflow/input/empty-permissions.yml b/testfiles/secureworkflow/input/empty-permissions.yml new file mode 100644 index 000000000..8b0e188b6 --- /dev/null +++ b/testfiles/secureworkflow/input/empty-permissions.yml @@ -0,0 +1,12 @@ +on: + push: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Node + uses: actions/setup-node@v1 \ No newline at end of file diff --git a/testfiles/secureworkflow/input/replaceactions.yml b/testfiles/secureworkflow/input/replaceactions.yml new file mode 100644 index 000000000..6cc0cca20 --- /dev/null +++ b/testfiles/secureworkflow/input/replaceactions.yml @@ -0,0 +1,33 @@ +name: Test Workflow +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - 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: '**/*' + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: github/super-linter@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DISABLE_ERRORS: true + - uses: tespkg/actions-cache/restore@v1 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + \ No newline at end of file diff --git a/testfiles/secureworkflow/output/allscenarios.yml b/testfiles/secureworkflow/output/allscenarios.yml index 99e0bb51f..3e3a578a2 100644 --- a/testfiles/secureworkflow/output/allscenarios.yml +++ b/testfiles/secureworkflow/output/allscenarios.yml @@ -14,7 +14,7 @@ jobs: statuses: write # for github/super-linter to mark status of each linter run runs-on: ubuntu-latest steps: - - name: Harden Runner + - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@ebacdc22ef6c2cfb85ee5ded8f2e640f4c776dd5 # v2.0.0 with: egress-policy: audit diff --git a/testfiles/secureworkflow/output/container-job.yml b/testfiles/secureworkflow/output/container-job.yml new file mode 100644 index 000000000..9ec39b1be --- /dev/null +++ b/testfiles/secureworkflow/output/container-job.yml @@ -0,0 +1,23 @@ +name: "Container job workflow" + +on: + push: + + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + container: + image: cgr.dev/chainguard/wolfi-base@sha256:91ed94ec4e72368a9b5113f2ffb1d8e783a91db489011a89d9fad3e3816a75ba + options: >- + --health-cmd pg_isready + --health-interval 10s + + steps: + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Run tests + run: npm test \ No newline at end of file diff --git a/testfiles/secureworkflow/output/empty-permissions.yml b/testfiles/secureworkflow/output/empty-permissions.yml new file mode 100644 index 000000000..1574d2ff4 --- /dev/null +++ b/testfiles/secureworkflow/output/empty-permissions.yml @@ -0,0 +1,21 @@ +on: + push: + +permissions: {} + +jobs: + test: + permissions: + contents: read # for actions/checkout to fetch code + runs-on: ubuntu-latest + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 + - name: Setup Node + uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e # v1.4.6 \ No newline at end of file diff --git a/testfiles/secureworkflow/output/missingaction.yml b/testfiles/secureworkflow/output/missingaction.yml index 20305effe..c1a1de4d2 100644 --- a/testfiles/secureworkflow/output/missingaction.yml +++ b/testfiles/secureworkflow/output/missingaction.yml @@ -8,7 +8,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - name: Harden Runner + - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@ebacdc22ef6c2cfb85ee5ded8f2e640f4c776dd5 # v2.0.0 with: egress-policy: audit diff --git a/testfiles/secureworkflow/output/noperms.yml b/testfiles/secureworkflow/output/noperms.yml index 4112b73a1..13ed91994 100644 --- a/testfiles/secureworkflow/output/noperms.yml +++ b/testfiles/secureworkflow/output/noperms.yml @@ -8,7 +8,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - name: Harden Runner + - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@ebacdc22ef6c2cfb85ee5ded8f2e640f4c776dd5 # v2.0.0 with: egress-policy: audit diff --git a/testfiles/secureworkflow/output/nopin.yml b/testfiles/secureworkflow/output/nopin.yml index 41b19b7d2..b83e77e86 100644 --- a/testfiles/secureworkflow/output/nopin.yml +++ b/testfiles/secureworkflow/output/nopin.yml @@ -14,8 +14,8 @@ jobs: statuses: write # for github/super-linter to mark status of each linter run runs-on: ubuntu-latest steps: - - name: Harden Runner - uses: step-security/harden-runner@v2 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ebacdc22ef6c2cfb85ee5ded8f2e640f4c776dd5 # v2.0.0 with: egress-policy: audit diff --git a/testfiles/secureworkflow/output/replaceactions.yml b/testfiles/secureworkflow/output/replaceactions.yml new file mode 100644 index 000000000..e8d874fd7 --- /dev/null +++ b/testfiles/secureworkflow/output/replaceactions.yml @@ -0,0 +1,43 @@ +name: Test Workflow +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ebacdc22ef6c2cfb85ee5ded8f2e640f4c776dd5 # v2.0.0 + with: + egress-policy: audit + + - uses: actions/checkout@v3 + - 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"]' + - uses: step-security/git-restore-mtime-action@c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0a1b2 # v2.1.0 + with: + pattern: '**/*' + + lint: + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ebacdc22ef6c2cfb85ee5ded8f2e640f4c776dd5 # v2.0.0 + with: + egress-policy: audit + + - uses: actions/checkout@544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9 # v1.2.0 + - uses: github/super-linter@34b2f8032d759425f6b42ea2e52231b33ae05401 # v3.17.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DISABLE_ERRORS: true + - uses: step-security/actions-cache/restore@d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0a1b2c3 # v1.0.0 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + \ No newline at end of file diff --git a/testfiles/toplevelperms/input/empty-permissions.yml b/testfiles/toplevelperms/input/empty-permissions.yml new file mode 100644 index 000000000..0f7c2ee95 --- /dev/null +++ b/testfiles/toplevelperms/input/empty-permissions.yml @@ -0,0 +1,9 @@ +name: "simple workflow" + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Run test + run: echo "test" \ No newline at end of file diff --git a/testfiles/toplevelperms/output/empty-permissions.yml b/testfiles/toplevelperms/output/empty-permissions.yml new file mode 100644 index 000000000..53143ae06 --- /dev/null +++ b/testfiles/toplevelperms/output/empty-permissions.yml @@ -0,0 +1,11 @@ +name: "simple workflow" + +permissions: {} + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Run test + run: echo "test" \ No newline at end of file diff --git a/workflow-templates/codeql.yml b/workflow-templates/codeql.yml index 9026e5f44..933093ec4 100644 --- a/workflow-templates/codeql.yml +++ b/workflow-templates/codeql.yml @@ -41,11 +41,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -55,7 +55,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -68,6 +68,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/workflow-templates/dependency-review.yml b/workflow-templates/dependency-review.yml index bd4d79445..d119b46dd 100644 --- a/workflow-templates/dependency-review.yml +++ b/workflow-templates/dependency-review.yml @@ -17,6 +17,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: 'Dependency Review' - uses: actions/dependency-review-action@v2 + uses: actions/dependency-review-action@v4 diff --git a/workflow-templates/scorecards.yml b/workflow-templates/scorecards.yml index 20164f965..55199c208 100644 --- a/workflow-templates/scorecards.yml +++ b/workflow-templates/scorecards.yml @@ -28,15 +28,20 @@ jobs: id-token: write contents: read actions: read + # To allow GraphQL ListCommits to work + issues: read + pull-requests: read + # To detect SAST tools + checks: read steps: - name: "Checkout code" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # v2.0.6 + uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 with: results_file: results.sarif results_format: sarif @@ -58,7 +63,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: SARIF file path: results.sarif @@ -66,6 +71,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: results.sarif