diff --git a/.codecov.yml b/.codecov.yml index b49dfb9e315..97cae9da52b 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -23,16 +23,15 @@ coverage: status: project: default: - enabled: true - # allowed to drop coverage and still result in a "success" commit status - threshold: null + informational: true if_not_found: success if_no_uploads: success if_ci_failed: error patch: default: - enabled: true - threshold: 90% + # patch coverage should be within 10% of existing coverage + target: auto + threshold: 10% if_not_found: success if_no_uploads: success if_ci_failed: error diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 73235106ba1..638ffd10930 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -52,12 +52,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout repository - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/depsreview.yml b/.github/workflows/depsreview.yml index 00d442a0e6d..bc90b255cf5 100644 --- a/.github/workflows/depsreview.yml +++ b/.github/workflows/depsreview.yml @@ -22,6 +22,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: 'Dependency Review' uses: actions/dependency-review-action@6c5ccdad469c9f8a2996bfecaec55a631a347034 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 59fc9ac4b29..a9d1225f0a5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -35,12 +35,12 @@ jobs: docs_only: ${{ steps.docs_only_check.outputs.docs_only }} steps: - name: Check out code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac #v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 #v4.1.0 with: fetch-depth: 2 # needed to diff changed files - id: files name: Get changed files - uses: tj-actions/changed-files@8e79ba7ab9fee9984275219aeb2c8db47bcb8a2d #v39.1.0 + uses: tj-actions/changed-files@db153baf731265ad02cd490b07f470e2d55e3345 #v39.2.1 with: files_ignore: '**.md' - id: docs_only_check @@ -70,12 +70,12 @@ jobs: steps: - name: Harden Runner if: (needs.docs_only_check.outputs.docs_only != 'true') - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Clone the code if: (needs.docs_only_check.outputs.docs_only != 'true') - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: Setup Go # needed for some of the Makefile evaluations, even if building happens in Docker if: (needs.docs_only_check.outputs.docs_only != 'true') uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 diff --git a/.github/workflows/gitlab.yml b/.github/workflows/gitlab.yml index 2a8bfc7a4d3..252a6e1a6d3 100644 --- a/.github/workflows/gitlab.yml +++ b/.github/workflows/gitlab.yml @@ -33,11 +33,11 @@ jobs: environment: gitlab steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Clone the code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} # head SHA if PR, else fallback to push SHA - name: Setup Go @@ -66,7 +66,7 @@ jobs: go mod download - name: Run GitLab tokenless E2E - uses: nick-invision/retry@943e742917ac94714d2f408a0e8320f2d1fcafcd # v2.8.3 + uses: nick-invision/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0 if: github.event_name == 'pull_request' with: max_attempts: 3 @@ -75,7 +75,7 @@ jobs: command: make e2e-gitlab - name: Run GitLab PAT E2E # skip if auth token is not available - uses: nick-invision/retry@943e742917ac94714d2f408a0e8320f2d1fcafcd # v2.8.3 + uses: nick-invision/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0 if: ${{ github.event_name == 'push' && github.actor != 'dependabot[bot]' }} env: GITLAB_AUTH_TOKEN: ${{ secrets.GITLAB_TOKEN }} diff --git a/.github/workflows/goreleaser.yaml b/.github/workflows/goreleaser.yaml index a70781eafdc..f57f127ba7c 100644 --- a/.github/workflows/goreleaser.yaml +++ b/.github/workflows/goreleaser.yaml @@ -34,12 +34,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: fetch-depth: 0 - name: Set up Go diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 828438bd616..01dfd7ac210 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs @@ -44,11 +44,11 @@ jobs: needs: [approve] steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Clone the code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: ref: ${{ github.event.pull_request.head.sha }} - name: Setup Go @@ -77,7 +77,7 @@ jobs: go mod download - name: Run GITHUB_TOKEN E2E #using retry because the GitHub token is being throttled. - uses: nick-invision/retry@943e742917ac94714d2f408a0e8320f2d1fcafcd # v2.8.3 + uses: nick-invision/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0 env: GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9256167a6d5..f6e4b0f2846 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,10 +19,10 @@ jobs: name: check-linter runs-on: ubuntu-latest steps: - - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + - uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 33b43bb28a2..bc6fbb870ad 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,11 +37,11 @@ jobs: contents: read steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Clone the code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: Setup Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: @@ -73,7 +73,7 @@ jobs: files: ./unit-coverage.out verbose: true - name: Run PAT Token E2E #using retry because the GitHub token is being throttled. - uses: nick-invision/retry@943e742917ac94714d2f408a0e8320f2d1fcafcd + uses: nick-invision/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 if: ${{ github.event_name != 'pull_request' && github.actor != 'dependabot[bot]' }} env: GITHUB_AUTH_TOKEN: ${{ secrets.GH_AUTH_TOKEN }} @@ -95,7 +95,7 @@ jobs: contents: read steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs @@ -117,7 +117,7 @@ jobs: restore-keys: | ${{ runner.os }}-go- - name: Clone the code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: fetch-depth: 0 - name: Setup Go @@ -127,7 +127,7 @@ jobs: check-latest: true cache: true - name: generate mocks - uses: nick-invision/retry@943e742917ac94714d2f408a0e8320f2d1fcafcd + uses: nick-invision/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 with: max_attempts: 3 retry_on: error @@ -143,11 +143,11 @@ jobs: contents: read steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Clone the code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: Setup Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: @@ -155,7 +155,7 @@ jobs: check-latest: true cache: true - name: generate docs - uses: nick-invision/retry@943e742917ac94714d2f408a0e8320f2d1fcafcd # v2.8.3 + uses: nick-invision/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0 with: max_attempts: 3 retry_on: error @@ -172,7 +172,7 @@ jobs: contents: read steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs @@ -182,7 +182,7 @@ jobs: version: ${{ env.PROTOC_VERSION }} repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Clone the code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: fetch-depth: 0 - name: Setup Go @@ -192,7 +192,7 @@ jobs: check-latest: true cache: true - name: build-proto - uses: nick-invision/retry@943e742917ac94714d2f408a0e8320f2d1fcafcd + uses: nick-invision/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 with: max_attempts: 3 retry_on: error @@ -221,7 +221,7 @@ jobs: contents: read steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Cache builds @@ -237,7 +237,7 @@ jobs: restore-keys: | ${{ runner.os }}-go- - name: Clone the code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: Setup Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: @@ -245,7 +245,7 @@ jobs: check-latest: true cache: true - name: Run build - uses: nick-invision/retry@943e742917ac94714d2f408a0e8320f2d1fcafcd # v2.8.3 + uses: nick-invision/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0 with: max_attempts: 3 retry_on: error @@ -260,7 +260,7 @@ jobs: contents: read steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs @@ -277,7 +277,7 @@ jobs: restore-keys: | ${{ runner.os }}-go- - name: Clone the code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: fetch-depth: 0 - name: Setup Go @@ -287,7 +287,7 @@ jobs: check-latest: true cache: true - name: Run build - uses: nick-invision/retry@943e742917ac94714d2f408a0e8320f2d1fcafcd + uses: nick-invision/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 with: max_attempts: 3 retry_on: error @@ -303,7 +303,7 @@ jobs: contents: read steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Install Protoc @@ -324,7 +324,7 @@ jobs: restore-keys: | ${{ runner.os }}-go- - name: Clone the code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: fetch-depth: 0 - name: Setup Go @@ -334,7 +334,7 @@ jobs: check-latest: true cache: true - name: Run build - uses: nick-invision/retry@943e742917ac94714d2f408a0e8320f2d1fcafcd + uses: nick-invision/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 with: max_attempts: 3 retry_on: error @@ -349,7 +349,7 @@ jobs: contents: read steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs @@ -359,7 +359,7 @@ jobs: version: ${{ env.PROTOC_VERSION }} repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Clone the code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: fetch-depth: 0 - name: Setup Go @@ -369,7 +369,7 @@ jobs: check-latest: true cache: true - name: Run build - uses: nick-invision/retry@943e742917ac94714d2f408a0e8320f2d1fcafcd + uses: nick-invision/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 with: max_attempts: 3 retry_on: error @@ -384,11 +384,11 @@ jobs: contents: read steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v2.2.0 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/publishimage.yml b/.github/workflows/publishimage.yml index 1efae292fcc..c4a63afafcf 100644 --- a/.github/workflows/publishimage.yml +++ b/.github/workflows/publishimage.yml @@ -35,12 +35,12 @@ jobs: COSIGN_EXPERIMENTAL: "true" steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Clone the code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: fetch-depth: 0 - name: Setup Go @@ -51,7 +51,7 @@ jobs: - name: install ko uses: ko-build/setup-ko@ace48d793556083a76f1e3e6068850c1f4a369aa # v0.6 - name: publishimage - uses: nick-invision/retry@943e742917ac94714d2f408a0e8320f2d1fcafcd + uses: nick-invision/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 with: max_attempts: 3 retry_on: error diff --git a/.github/workflows/scorecard-analysis.yml b/.github/workflows/scorecard-analysis.yml index 6e4c9311503..88add80259f 100644 --- a/.github/workflows/scorecard-analysis.yml +++ b/.github/workflows/scorecard-analysis.yml @@ -22,10 +22,10 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: "Run analysis" - uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0 + uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0 with: results_file: results.sarif results_format: sarif diff --git a/.github/workflows/slsa-goreleaser.yml b/.github/workflows/slsa-goreleaser.yml index 6a7fe8b8e8c..133e9ec360d 100644 --- a/.github/workflows/slsa-goreleaser.yml +++ b/.github/workflows/slsa-goreleaser.yml @@ -19,7 +19,7 @@ jobs: go-binary-name: ${{ steps.build.outputs.go-binary-name }} steps: - id: checkout - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: fetch-depth: 0 - id: ldflags diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 3f051893cb1..85fcd788af3 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -27,14 +27,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v3.0.18 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: 'Stale issue message - this issue will be closed in 7 days' + stale-issue-message: 'This issue is stale because it has been open for 60 days with no activity.' stale-pr-message: 'Stale pull request message' stale-issue-label: 'no-issue-activity' exempt-issue-labels: 'priority,bug,good first issue' @@ -43,5 +43,5 @@ jobs: days-before-pr-stale: '10' days-before-pr-close: '20' days-before-issue-stale: '60' - days-before-issue-close: '7' + days-before-issue-close: -1 operations-per-run: '100' diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 5a90c9153fb..dff08db2f2f 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs diff --git a/Dockerfile b/Dockerfile index 3bfdd1dde8e..e56cf503c69 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM golang:1.21@sha256:ec457a2fcd235259273428a24e09900c496d0c52207266f96a330062a01e3622 AS base +FROM golang:1.21@sha256:e9ebfe932adeff65af5338236f0b0604c86b143c1bff3e1d0551d8f6196ab5c5 AS base WORKDIR /src ENV CGO_ENABLED=0 COPY go.* ./ @@ -24,6 +24,6 @@ ARG TARGETOS ARG TARGETARCH RUN CGO_ENABLED=0 make build-scorecard -FROM gcr.io/distroless/base:nonroot@sha256:27647a684d554b6640e32c549dacb3c898c2632fedd0e822b6ffdc24c1c18150 +FROM gcr.io/distroless/base:nonroot@sha256:29da700a46816467c7cb91058f53eac4170a4a25ac8551d316d9fd38e2c58bdf COPY --from=build /src/scorecard / ENTRYPOINT [ "/scorecard" ] diff --git a/README.md b/README.md index b43d91cf1a6..bda6efed521 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ - [What Is Scorecard?](#what-is-scorecard) - [Prominent Scorecard Users](#prominent-scorecard-users) +- [View a Project's Score](#view-a-projects-score) - [Scorecard's Public Data](#public-data) ## Using Scorecard @@ -91,6 +92,17 @@ metrics. Prominent projects that use Scorecard include: - [sos.dev](https://sos.dev) - [deps.dev](https://deps.dev) +### View a Project's Score + +To see scores for projects regularly scanned by Scorecard, navigate to the webviewer, replacing the placeholder text with the platform, user/org, and repository name: +https://securityscorecards.dev/viewer/?uri=.com//. + +For example: + - [https://securityscorecards.dev/viewer/?uri=github.com/ossf/scorecard](https://securityscorecards.dev/viewer/?uri=github.com/ossf/scorecard) + - [https://securityscorecards.dev/viewer/?uri=gitlab.com/fdroid/fdroidclient](https://securityscorecards.dev/viewer/?uri=gitlab.com/fdroid/fdroidclient) + +To view scores for projects not included in the webviewer, use the [Scorecard CLI](#scorecard-command-line-interface). + ### Public Data We run a weekly Scorecard scan of the 1 million most critical open source diff --git a/attestor/Dockerfile b/attestor/Dockerfile index c36671691ea..b38f745fbd9 100644 --- a/attestor/Dockerfile +++ b/attestor/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM golang:1.21@sha256:ec457a2fcd235259273428a24e09900c496d0c52207266f96a330062a01e3622 AS base +FROM golang:1.21@sha256:e9ebfe932adeff65af5338236f0b0604c86b143c1bff3e1d0551d8f6196ab5c5 AS base WORKDIR /src/scorecard COPY . ./ diff --git a/checker/check_result.go b/checker/check_result.go index edb98d0fe5d..00fc9172da1 100644 --- a/checker/check_result.go +++ b/checker/check_result.go @@ -16,6 +16,7 @@ package checker import ( + "errors" "fmt" "math" @@ -50,6 +51,10 @@ const ( DetailDebug ) +// errSuccessTotal indicates a runtime error because number of success cases should +// be smaller than the total cases to create a proportional score. +var errSuccessTotal = errors.New("unexpected number of success is higher than total") + // CheckResult captures result from a check run. // //nolint:govet @@ -88,6 +93,14 @@ type LogMessage struct { Remediation *rule.Remediation // Remediation information, if any. } +// ProportionalScoreWeighted is a structure that contains +// the fields to calculate weighted proportional scores. +type ProportionalScoreWeighted struct { + Success int + Total int + Weight int +} + // CreateProportionalScore creates a proportional score. func CreateProportionalScore(success, total int) int { if total == 0 { @@ -97,6 +110,40 @@ func CreateProportionalScore(success, total int) int { return int(math.Min(float64(MaxResultScore*success/total), float64(MaxResultScore))) } +// CreateProportionalScoreWeighted creates the proportional score +// between multiple successes over the total, but some proportions +// are worth more. +func CreateProportionalScoreWeighted(scores ...ProportionalScoreWeighted) (int, error) { + var ws, wt int + allWeightsZero := true + noScoreGroups := true + for _, score := range scores { + if score.Success > score.Total { + return InconclusiveResultScore, fmt.Errorf("%w: %d, %d", errSuccessTotal, score.Success, score.Total) + } + if score.Total == 0 { + continue // Group with 0 total, does not count for score + } + noScoreGroups = false + if score.Weight != 0 { + allWeightsZero = false + } + // Group with zero weight, adds nothing to the score + + ws += score.Success * score.Weight + wt += score.Total * score.Weight + } + if noScoreGroups { + return InconclusiveResultScore, nil + } + // If has score groups but no groups matter to the score, result in max score + if allWeightsZero { + return MaxResultScore, nil + } + + return int(math.Min(float64(MaxResultScore*ws/wt), float64(MaxResultScore))), nil +} + // AggregateScores adds up all scores // and normalizes the result. // Each score contributes equally. diff --git a/checker/check_result_test.go b/checker/check_result_test.go index 291f0696cde..1ec48850340 100644 --- a/checker/check_result_test.go +++ b/checker/check_result_test.go @@ -139,6 +139,273 @@ func TestCreateProportionalScore(t *testing.T) { } } +func TestCreateProportionalScoreWeighted(t *testing.T) { + t.Parallel() + type want struct { + score int + err bool + } + tests := []struct { + name string + scores []ProportionalScoreWeighted + want want + }{ + { + name: "max result with 1 group and normal weight", + scores: []ProportionalScoreWeighted{ + { + Success: 1, + Total: 1, + Weight: 10, + }, + }, + want: want{ + score: 10, + }, + }, + { + name: "min result with 1 group and normal weight", + scores: []ProportionalScoreWeighted{ + { + Success: 0, + Total: 1, + Weight: 10, + }, + }, + want: want{ + score: 0, + }, + }, + { + name: "partial result with 1 group and normal weight", + scores: []ProportionalScoreWeighted{ + { + Success: 2, + Total: 10, + Weight: 10, + }, + }, + want: want{ + score: 2, + }, + }, + { + name: "partial result with 2 groups and normal weights", + scores: []ProportionalScoreWeighted{ + { + Success: 2, + Total: 10, + Weight: 10, + }, + { + Success: 8, + Total: 10, + Weight: 10, + }, + }, + want: want{ + score: 5, + }, + }, + { + name: "partial result with 2 groups and odd weights", + scores: []ProportionalScoreWeighted{ + { + Success: 2, + Total: 10, + Weight: 8, + }, + { + Success: 8, + Total: 10, + Weight: 2, + }, + }, + want: want{ + score: 3, + }, + }, + { + name: "all groups with 0 weight, no groups matter for the score, results in max score", + scores: []ProportionalScoreWeighted{ + { + Success: 1, + Total: 1, + Weight: 0, + }, + { + Success: 1, + Total: 1, + Weight: 0, + }, + }, + want: want{ + score: 10, + }, + }, + { + name: "not all groups with 0 weight, only groups with weight matter to the score", + scores: []ProportionalScoreWeighted{ + { + Success: 1, + Total: 1, + Weight: 0, + }, + { + Success: 2, + Total: 10, + Weight: 8, + }, + { + Success: 8, + Total: 10, + Weight: 2, + }, + }, + want: want{ + score: 3, + }, + }, + { + name: "no total, results in inconclusive score", + scores: []ProportionalScoreWeighted{ + { + Success: 0, + Total: 0, + Weight: 10, + }, + }, + want: want{ + score: -1, + }, + }, + { + name: "some groups with 0 total, only groups with total matter to the score", + scores: []ProportionalScoreWeighted{ + { + Success: 0, + Total: 0, + Weight: 10, + }, + { + Success: 2, + Total: 10, + Weight: 10, + }, + }, + want: want{ + score: 2, + }, + }, + { + name: "any group with number of successes higher than total, results in inconclusive score and error", + scores: []ProportionalScoreWeighted{ + { + Success: 1, + Total: 0, + Weight: 10, + }, + }, + want: want{ + score: -1, + err: true, + }, + }, + { + name: "only groups with weight and total matter to the score", + scores: []ProportionalScoreWeighted{ + { + Success: 1, + Total: 1, + Weight: 0, + }, + { + Success: 0, + Total: 0, + Weight: 10, + }, + { + Success: 2, + Total: 10, + Weight: 8, + }, + { + Success: 8, + Total: 10, + Weight: 2, + }, + }, + want: want{ + score: 3, + }, + }, + { + name: "only groups with weight and total matter to the score but no groups have success, results in min score", + scores: []ProportionalScoreWeighted{ + { + Success: 1, + Total: 1, + Weight: 0, + }, + { + Success: 0, + Total: 0, + Weight: 10, + }, + { + Success: 0, + Total: 10, + Weight: 8, + }, + { + Success: 0, + Total: 10, + Weight: 2, + }, + }, + want: want{ + score: 0, + }, + }, + { + name: "group with 0 weight counts as max score and group with 0 total does not count", + scores: []ProportionalScoreWeighted{ + { + Success: 2, + Total: 8, + Weight: 0, + }, + { + Success: 0, + Total: 0, + Weight: 10, + }, + }, + want: want{ + score: 10, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := CreateProportionalScoreWeighted(tt.scores...) + if err != nil && !tt.want.err { + t.Errorf("CreateProportionalScoreWeighted unexpected error '%v'", err) + t.Fail() + } + if err == nil && tt.want.err { + t.Errorf("CreateProportionalScoreWeighted expected error and got none") + t.Fail() + } + if got != tt.want.score { + t.Errorf("CreateProportionalScoreWeighted() = %v, want %v", got, tt.want.score) + } + }) + } +} + func TestNormalizeReason(t *testing.T) { t.Parallel() type args struct { diff --git a/checker/raw_result.go b/checker/raw_result.go index 2acaee17a47..1b5b980f37a 100644 --- a/checker/raw_result.go +++ b/checker/raw_result.go @@ -25,25 +25,25 @@ import ( // is applied. // nolint type RawResults struct { - PackagingResults PackagingData - CIIBestPracticesResults CIIBestPracticesData - DangerousWorkflowResults DangerousWorkflowData - VulnerabilitiesResults VulnerabilitiesData BinaryArtifactResults BinaryArtifactData - SecurityPolicyResults SecurityPolicyData - DependencyUpdateToolResults DependencyUpdateToolData BranchProtectionResults BranchProtectionsData + CIIBestPracticesResults CIIBestPracticesData + CITestResults CITestData CodeReviewResults CodeReviewData - PinningDependenciesResults PinningDependenciesData - WebhookResults WebhooksData ContributorsResults ContributorsData - MaintainedResults MaintainedData - SignedReleasesResults SignedReleasesData + DangerousWorkflowResults DangerousWorkflowData + DependencyUpdateToolResults DependencyUpdateToolData FuzzingResults FuzzingData LicenseResults LicenseData - TokenPermissionsResults TokenPermissionsData - CITestResults CITestData + MaintainedResults MaintainedData Metadata MetadataData + PackagingResults PackagingData + PinningDependenciesResults PinningDependenciesData + SecurityPolicyResults SecurityPolicyData + SignedReleasesResults SignedReleasesData + TokenPermissionsResults TokenPermissionsData + VulnerabilitiesResults VulnerabilitiesData + WebhookResults WebhooksData } type MetadataData struct { @@ -121,6 +121,7 @@ type Dependency struct { PinnedAt *string Location *File Msg *string // Only for debug messages. + Pinned *bool Type DependencyUseType } diff --git a/checks/branch_protection_test.go b/checks/branch_protection_test.go index 929c20768c8..035120a1486 100644 --- a/checks/branch_protection_test.go +++ b/checks/branch_protection_test.go @@ -134,7 +134,7 @@ func TestReleaseAndDevBranchProtected(t *testing.T) { name: "Only development branch", expected: scut.TestReturn{ Error: nil, - Score: 2, + Score: 3, NumberOfWarn: 7, NumberOfInfo: 2, NumberOfDebug: 0, @@ -173,7 +173,7 @@ func TestReleaseAndDevBranchProtected(t *testing.T) { name: "Take worst of release and development", expected: scut.TestReturn{ Error: nil, - Score: 2, + Score: 4, NumberOfWarn: 9, NumberOfInfo: 10, NumberOfDebug: 0, @@ -285,7 +285,7 @@ func TestReleaseAndDevBranchProtected(t *testing.T) { name: "Ignore a non-branch targetcommitish", expected: scut.TestReturn{ Error: nil, - Score: 2, + Score: 3, NumberOfWarn: 7, NumberOfInfo: 2, NumberOfDebug: 0, diff --git a/checks/evaluation/branch_protection.go b/checks/evaluation/branch_protection.go index 7703cab920c..1a66347c663 100644 --- a/checks/evaluation/branch_protection.go +++ b/checks/evaluation/branch_protection.go @@ -25,7 +25,7 @@ import ( const ( minReviews = 2 // Points incremented at each level. - adminNonAdminBasicLevel = 3 // Level 1. + basicLevel = 3 // Level 1. adminNonAdminReviewLevel = 3 // Level 2. nonAdminContextLevel = 2 // Level 3. nonAdminThoroughReviewLevel = 1 // Level 4. @@ -35,7 +35,6 @@ const ( type scoresInfo struct { basic int - adminBasic int review int adminReview int context int @@ -71,7 +70,6 @@ func BranchProtection(name string, dl checker.DetailLogger, }) } score.scores.basic, score.maxes.basic = basicNonAdminProtection(&b, dl) - score.scores.adminBasic, score.maxes.adminBasic = basicAdminProtection(&b, dl) score.scores.review, score.maxes.review = nonAdminReviewProtection(&b) score.scores.adminReview, score.maxes.adminReview = adminReviewProtection(&b, dl) score.scores.context, score.maxes.context = nonAdminContextProtection(&b, dl) @@ -114,15 +112,6 @@ func computeNonAdminBasicScore(scores []levelScore) int { return score } -func computeAdminBasicScore(scores []levelScore) int { - score := 0 - for i := range scores { - s := scores[i] - score += s.scores.adminBasic - } - return score -} - func computeNonAdminReviewScore(scores []levelScore) int { score := 0 for i := range scores { @@ -194,12 +183,9 @@ func computeScore(scores []levelScore) (int, error) { // First, check if they all pass the basic (admin and non-admin) checks. maxBasicScore := maxScore.basic * len(scores) - maxAdminBasicScore := maxScore.adminBasic * len(scores) basicScore := computeNonAdminBasicScore(scores) - adminBasicScore := computeAdminBasicScore(scores) - score += noarmalizeScore(basicScore+adminBasicScore, maxBasicScore+maxAdminBasicScore, adminNonAdminBasicLevel) - if basicScore != maxBasicScore || - adminBasicScore != maxAdminBasicScore { + score += noarmalizeScore(basicScore, maxBasicScore, basicLevel) + if basicScore != maxBasicScore { return int(score), nil } @@ -308,30 +294,6 @@ func basicNonAdminProtection(branch *clients.BranchRef, dl checker.DetailLogger) return score, max } -func basicAdminProtection(branch *clients.BranchRef, dl checker.DetailLogger) (int, int) { - score := 0 - max := 0 - // Only log information if the branch is protected. - log := branch.Protected != nil && *branch.Protected - - // nil typically means we do not have access to the value. - if branch.BranchProtectionRule.EnforceAdmins != nil { - // Note: we don't inrecase max possible score for non-admin viewers. - max++ - switch *branch.BranchProtectionRule.EnforceAdmins { - case true: - info(dl, log, "settings apply to administrators on branch '%s'", *branch.Name) - score++ - case false: - warn(dl, log, "settings do not apply to administrators on branch '%s'", *branch.Name) - } - } else { - debug(dl, log, "unable to retrieve whether or not settings apply to administrators on branch '%s'", *branch.Name) - } - - return score, max -} - func nonAdminContextProtection(branch *clients.BranchRef, dl checker.DetailLogger) (int, int) { score := 0 max := 0 @@ -422,6 +384,22 @@ func adminThoroughReviewProtection(branch *clients.BranchRef, dl checker.DetailL } else { debug(dl, log, "unable to retrieve review dismissal on branch '%s'", *branch.Name) } + + // nil typically means we do not have access to the value. + if branch.BranchProtectionRule.EnforceAdmins != nil { + // Note: we don't inrecase max possible score for non-admin viewers. + max++ + switch *branch.BranchProtectionRule.EnforceAdmins { + case true: + info(dl, log, "settings apply to administrators on branch '%s'", *branch.Name) + score++ + case false: + warn(dl, log, "settings do not apply to administrators on branch '%s'", *branch.Name) + } + } else { + debug(dl, log, "unable to retrieve whether or not settings apply to administrators on branch '%s'", *branch.Name) + } + return score, max } diff --git a/checks/evaluation/branch_protection_test.go b/checks/evaluation/branch_protection_test.go index 5fd5e541edb..2b45d116714 100644 --- a/checks/evaluation/branch_protection_test.go +++ b/checks/evaluation/branch_protection_test.go @@ -25,7 +25,6 @@ import ( func testScore(branch *clients.BranchRef, codeownersFiles []string, dl checker.DetailLogger) (int, error) { var score levelScore score.scores.basic, score.maxes.basic = basicNonAdminProtection(branch, dl) - score.scores.adminBasic, score.maxes.adminBasic = basicAdminProtection(branch, dl) score.scores.review, score.maxes.review = nonAdminReviewProtection(branch) score.scores.adminReview, score.maxes.adminReview = adminReviewProtection(branch, dl) score.scores.context, score.maxes.context = nonAdminContextProtection(branch, dl) @@ -53,7 +52,7 @@ func TestIsBranchProtected(t *testing.T) { name: "Nothing is enabled", expected: scut.TestReturn{ Error: nil, - Score: 2, + Score: 3, NumberOfWarn: 7, NumberOfInfo: 2, NumberOfDebug: 0, @@ -98,7 +97,7 @@ func TestIsBranchProtected(t *testing.T) { name: "Required status check enabled", expected: scut.TestReturn{ Error: nil, - Score: 2, + Score: 4, NumberOfWarn: 5, NumberOfInfo: 4, NumberOfDebug: 0, @@ -129,7 +128,7 @@ func TestIsBranchProtected(t *testing.T) { name: "Required status check enabled without checking for status string", expected: scut.TestReturn{ Error: nil, - Score: 2, + Score: 4, NumberOfWarn: 6, NumberOfInfo: 3, NumberOfDebug: 0, @@ -160,7 +159,7 @@ func TestIsBranchProtected(t *testing.T) { name: "Required pull request enabled", expected: scut.TestReturn{ Error: nil, - Score: 2, + Score: 4, NumberOfWarn: 6, NumberOfInfo: 3, NumberOfDebug: 0, @@ -222,7 +221,7 @@ func TestIsBranchProtected(t *testing.T) { name: "Required linear history enabled", expected: scut.TestReturn{ Error: nil, - Score: 2, + Score: 3, NumberOfWarn: 6, NumberOfInfo: 3, NumberOfDebug: 0, diff --git a/checks/evaluation/pinned_dependencies.go b/checks/evaluation/pinned_dependencies.go index c05038fa7ec..c0af9515214 100644 --- a/checks/evaluation/pinned_dependencies.go +++ b/checks/evaluation/pinned_dependencies.go @@ -15,26 +15,19 @@ package evaluation import ( - "errors" "fmt" "github.com/ossf/scorecard/v4/checker" "github.com/ossf/scorecard/v4/checks/fileparser" sce "github.com/ossf/scorecard/v4/errors" - "github.com/ossf/scorecard/v4/finding" "github.com/ossf/scorecard/v4/remediation" "github.com/ossf/scorecard/v4/rule" ) -var errInvalidValue = errors.New("invalid value") - -type pinnedResult int - -const ( - pinnedUndefined pinnedResult = iota - pinned - notPinned -) +type pinnedResult struct { + pinned int + total int +} // Structure to host information about pinned github // or third party dependencies. @@ -43,6 +36,20 @@ type worklowPinningResult struct { gitHubOwned pinnedResult } +// Weights used for proportional score. +// This defines the priority of pinning a dependency over other dependencies. +// The dependencies from all ecosystems are equally prioritized except +// for GitHub Actions. GitHub Actions can be GitHub-owned or from third-party +// development. The GitHub Actions ecosystem has equal priority compared to other +// ecosystems, but, within GitHub Actions, pinning third-party actions has more +// priority than pinning GitHub-owned actions. +// https://github.com/ossf/scorecard/issues/802 +const ( + gitHubOwnedActionWeight int = 2 + thirdPartyActionWeight int = 8 + normalWeight int = gitHubOwnedActionWeight + thirdPartyActionWeight +) + // PinningDependencies applies the score policy for the Pinned-Dependencies check. func PinningDependencies(name string, c *checker.CheckRequest, r *checker.PinningDependenciesData, @@ -70,7 +77,6 @@ func PinningDependencies(name string, c *checker.CheckRequest, }) continue } - if rr.Msg != nil { dl.Debug(&checker.LogMessage{ Path: rr.Location.Path, @@ -80,7 +86,20 @@ func PinningDependencies(name string, c *checker.CheckRequest, Text: *rr.Msg, Snippet: rr.Location.Snippet, }) - } else { + continue + } + if rr.Pinned == nil { + dl.Debug(&checker.LogMessage{ + Path: rr.Location.Path, + Type: rr.Location.Type, + Offset: rr.Location.Offset, + EndOffset: rr.Location.EndOffset, + Text: fmt.Sprintf("%s has empty Pinned field", rr.Type), + Snippet: rr.Location.Snippet, + }) + continue + } + if !*rr.Pinned { dl.Warn(&checker.LogMessage{ Path: rr.Location.Path, Type: rr.Location.Type, @@ -90,67 +109,37 @@ func PinningDependencies(name string, c *checker.CheckRequest, Snippet: rr.Location.Snippet, Remediation: generateRemediation(remediationMetadata, &rr), }) - - // Update the pinning status. - updatePinningResults(&rr, &wp, pr) } + // Update the pinning status. + updatePinningResults(&rr, &wp, pr) } // Generate scores and Info results. - // GitHub actions. - actionScore, err := createReturnForIsGitHubActionsWorkflowPinned(wp, dl) - if err != nil { - return checker.CreateRuntimeErrorResult(name, err) - } - - // Docker files. - dockerFromScore, err := createReturnForIsDockerfilePinned(pr, dl) - if err != nil { - return checker.CreateRuntimeErrorResult(name, err) - } - - // Docker downloads. - dockerDownloadScore, err := createReturnForIsDockerfileFreeOfInsecureDownloads(pr, dl) - if err != nil { - return checker.CreateRuntimeErrorResult(name, err) - } - - // Script downloads. - scriptScore, err := createReturnForIsShellScriptFreeOfInsecureDownloads(pr, dl) - if err != nil { - return checker.CreateRuntimeErrorResult(name, err) - } - - // Pip installs. - pipScore, err := createReturnForIsPipInstallPinned(pr, dl) - if err != nil { - return checker.CreateRuntimeErrorResult(name, err) + var scores []checker.ProportionalScoreWeighted + // Go through all dependency types + // GitHub Actions need to be handled separately since they are not in pr + scores = append(scores, createScoreForGitHubActionsWorkflow(&wp, dl)...) + // Only exisiting dependencies will be found in pr + // We will only score the ecosystem if there are dependencies + // This results in only existing ecosystems being included in the final score + for t := range pr { + logPinnedResult(dl, pr[t], string(t)) + scores = append(scores, checker.ProportionalScoreWeighted{ + Success: pr[t].pinned, + Total: pr[t].total, + Weight: normalWeight, + }) } - // Npm installs. - npmScore, err := createReturnForIsNpmInstallPinned(pr, dl) - if err != nil { - return checker.CreateRuntimeErrorResult(name, err) + if len(scores) == 0 { + return checker.CreateInconclusiveResult(name, "no dependencies found") } - // Go installs. - goScore, err := createReturnForIsGoInstallPinned(pr, dl) + score, err := checker.CreateProportionalScoreWeighted(scores...) if err != nil { return checker.CreateRuntimeErrorResult(name, err) } - // Scores may be inconclusive. - actionScore = maxScore(0, actionScore) - dockerFromScore = maxScore(0, dockerFromScore) - dockerDownloadScore = maxScore(0, dockerDownloadScore) - scriptScore = maxScore(0, scriptScore) - pipScore = maxScore(0, pipScore) - npmScore = maxScore(0, npmScore) - goScore = maxScore(0, goScore) - - score := checker.AggregateScores(actionScore, dockerFromScore, - dockerDownloadScore, scriptScore, pipScore, npmScore, goScore) - if score == checker.MaxResultScore { return checker.CreateMaxScoreResult(name, "all dependencies are pinned") } @@ -177,13 +166,13 @@ func updatePinningResults(rr *checker.Dependency, // Note: `Snippet` contains `action/name@xxx`, so we cna use it to infer // if it's a GitHub-owned action or not. gitHubOwned := fileparser.IsGitHubOwnedAction(rr.Location.Snippet) - addWorkflowPinnedResult(wp, false, gitHubOwned) + addWorkflowPinnedResult(rr, wp, gitHubOwned) return } // Update other result types. - var p pinnedResult - addPinnedResult(&p, false) + p := pr[rr.Type] + addPinnedResult(rr, &p) pr[rr.Type] = p } @@ -192,7 +181,7 @@ func generateText(rr *checker.Dependency) string { // Check if we are dealing with a GitHub action or a third-party one. gitHubOwned := fileparser.IsGitHubOwnedAction(rr.Location.Snippet) owner := generateOwnerToDisplay(gitHubOwned) - return fmt.Sprintf("%s %s not pinned by hash", owner, rr.Type) + return fmt.Sprintf("%s not pinned by hash", owner) } return fmt.Sprintf("%s not pinned by hash", rr.Type) @@ -200,149 +189,69 @@ func generateText(rr *checker.Dependency) string { func generateOwnerToDisplay(gitHubOwned bool) string { if gitHubOwned { - return "GitHub-owned" + return fmt.Sprintf("GitHub-owned %s", checker.DependencyUseTypeGHAction) } - return "third-party" + return fmt.Sprintf("third-party %s", checker.DependencyUseTypeGHAction) } -// TODO(laurent): need to support GCB pinning. -func maxScore(s1, s2 int) int { - if s1 > s2 { - return s1 +func addPinnedResult(rr *checker.Dependency, r *pinnedResult) { + if *rr.Pinned { + r.pinned += 1 } - return s2 + r.total += 1 } -// For the 'to' param, true means the file is pinning dependencies (or there are no dependencies), -// false means there are unpinned dependencies. -func addPinnedResult(r *pinnedResult, to bool) { - // If the result is `notPinned`, we keep it. - // In other cases, we always update the result. - if *r == notPinned { - return - } - - switch to { - case true: - *r = pinned - case false: - *r = notPinned - } -} - -func addWorkflowPinnedResult(w *worklowPinningResult, to, isGitHub bool) { +func addWorkflowPinnedResult(rr *checker.Dependency, w *worklowPinningResult, isGitHub bool) { if isGitHub { - addPinnedResult(&w.gitHubOwned, to) + addPinnedResult(rr, &w.gitHubOwned) } else { - addPinnedResult(&w.thirdParties, to) + addPinnedResult(rr, &w.thirdParties) } } -// Create the result for scripts. -func createReturnForIsShellScriptFreeOfInsecureDownloads(pr map[checker.DependencyUseType]pinnedResult, - dl checker.DetailLogger, -) (int, error) { - return createReturnValues(pr, checker.DependencyUseTypeDownloadThenRun, - "no insecure (not pinned by hash) dependency downloads found in shell scripts", - dl) -} - -// Create the result for docker containers. -func createReturnForIsDockerfilePinned(pr map[checker.DependencyUseType]pinnedResult, - dl checker.DetailLogger, -) (int, error) { - return createReturnValues(pr, checker.DependencyUseTypeDockerfileContainerImage, - "Dockerfile dependencies are pinned", - dl) -} - -// Create the result for docker commands. -func createReturnForIsDockerfileFreeOfInsecureDownloads(pr map[checker.DependencyUseType]pinnedResult, - dl checker.DetailLogger, -) (int, error) { - return createReturnValues(pr, checker.DependencyUseTypeDownloadThenRun, - "no insecure (not pinned by hash) dependency downloads found in Dockerfiles", - dl) -} - -// Create the result for pip install commands. -func createReturnForIsPipInstallPinned(pr map[checker.DependencyUseType]pinnedResult, - dl checker.DetailLogger, -) (int, error) { - return createReturnValues(pr, checker.DependencyUseTypePipCommand, - "Pip installs are pinned", - dl) +func logPinnedResult(dl checker.DetailLogger, p pinnedResult, name string) { + dl.Info(&checker.LogMessage{ + Text: fmt.Sprintf("%3d out of %3d %s dependencies pinned", p.pinned, p.total, name), + }) } -// Create the result for npm install commands. -func createReturnForIsNpmInstallPinned(pr map[checker.DependencyUseType]pinnedResult, - dl checker.DetailLogger, -) (int, error) { - return createReturnValues(pr, checker.DependencyUseTypeNpmCommand, - "npm installs are pinned", - dl) -} - -// Create the result for go install commands. -func createReturnForIsGoInstallPinned(pr map[checker.DependencyUseType]pinnedResult, - dl checker.DetailLogger, -) (int, error) { - return createReturnValues(pr, checker.DependencyUseTypeGoCommand, - "go installs are pinned", - dl) -} - -func createReturnValues(pr map[checker.DependencyUseType]pinnedResult, - t checker.DependencyUseType, infoMsg string, - dl checker.DetailLogger, -) (int, error) { - // Note: we don't check if the entry exists, - // as it will have the default value which is handled in the switch statement. - //nolint - r, _ := pr[t] - switch r { - default: - return checker.InconclusiveResultScore, fmt.Errorf("%w: %v", errInvalidValue, r) - case pinned, pinnedUndefined: - dl.Info(&checker.LogMessage{ - Text: infoMsg, - }) - return checker.MaxResultScore, nil - case notPinned: - // No logging needed as it's done by the checks. - return checker.MinResultScore, nil +func createScoreForGitHubActionsWorkflow(wp *worklowPinningResult, dl checker.DetailLogger, +) []checker.ProportionalScoreWeighted { + if wp.gitHubOwned.total == 0 && wp.thirdParties.total == 0 { + return []checker.ProportionalScoreWeighted{} } -} - -// Create the result. -func createReturnForIsGitHubActionsWorkflowPinned(wp worklowPinningResult, dl checker.DetailLogger) (int, error) { - return createReturnValuesForGitHubActionsWorkflowPinned(wp, - fmt.Sprintf("%ss are pinned", checker.DependencyUseTypeGHAction), - dl) -} - -func createReturnValuesForGitHubActionsWorkflowPinned(r worklowPinningResult, infoMsg string, - dl checker.DetailLogger, -) (int, error) { - score := checker.MinResultScore - - if r.gitHubOwned != notPinned { - score += 2 - dl.Info(&checker.LogMessage{ - Type: finding.FileTypeSource, - Offset: checker.OffsetDefault, - Text: fmt.Sprintf("%s %s", "GitHub-owned", infoMsg), - }) + if wp.gitHubOwned.total != 0 && wp.thirdParties.total != 0 { + logPinnedResult(dl, wp.gitHubOwned, generateOwnerToDisplay(true)) + logPinnedResult(dl, wp.thirdParties, generateOwnerToDisplay(false)) + return []checker.ProportionalScoreWeighted{ + { + Success: wp.gitHubOwned.pinned, + Total: wp.gitHubOwned.total, + Weight: gitHubOwnedActionWeight, + }, + { + Success: wp.thirdParties.pinned, + Total: wp.thirdParties.total, + Weight: thirdPartyActionWeight, + }, + } } - - if r.thirdParties != notPinned { - score += 8 - dl.Info(&checker.LogMessage{ - Type: finding.FileTypeSource, - Offset: checker.OffsetDefault, - Text: fmt.Sprintf("%s %s", "Third-party", infoMsg), - }) + if wp.gitHubOwned.total != 0 { + logPinnedResult(dl, wp.gitHubOwned, generateOwnerToDisplay(true)) + return []checker.ProportionalScoreWeighted{ + { + Success: wp.gitHubOwned.pinned, + Total: wp.gitHubOwned.total, + Weight: normalWeight, + }, + } + } + logPinnedResult(dl, wp.thirdParties, generateOwnerToDisplay(false)) + return []checker.ProportionalScoreWeighted{ + { + Success: wp.thirdParties.pinned, + Total: wp.thirdParties.total, + Weight: normalWeight, + }, } - - return score, nil } diff --git a/checks/evaluation/pinned_dependencies_test.go b/checks/evaluation/pinned_dependencies_test.go index 0a40c0da9a0..3efb32c3901 100644 --- a/checks/evaluation/pinned_dependencies_test.go +++ b/checks/evaluation/pinned_dependencies_test.go @@ -20,67 +20,208 @@ import ( "github.com/google/go-cmp/cmp" "github.com/ossf/scorecard/v4/checker" + sce "github.com/ossf/scorecard/v4/errors" scut "github.com/ossf/scorecard/v4/utests" ) -func Test_createReturnValuesForGitHubActionsWorkflowPinned(t *testing.T) { +func Test_createScoreForGitHubActionsWorkflow(t *testing.T) { t.Parallel() //nolint - type args struct { - r worklowPinningResult - infoMsg string - dl checker.DetailLogger - } - //nolint tests := []struct { - name string - args args - want int + name string + r worklowPinningResult + scores []checker.ProportionalScoreWeighted }{ { - name: "both actions workflow pinned", - args: args{ - r: worklowPinningResult{ - thirdParties: 1, - gitHubOwned: 1, + name: "GitHub-owned and Third-Party actions pinned", + r: worklowPinningResult{ + gitHubOwned: pinnedResult{ + pinned: 1, + total: 1, + }, + thirdParties: pinnedResult{ + pinned: 1, + total: 1, + }, + }, + scores: []checker.ProportionalScoreWeighted{ + { + Success: 1, + Total: 1, + Weight: 2, + }, + { + Success: 1, + Total: 1, + Weight: 8, }, - dl: &scut.TestDetailLogger{}, }, - want: 10, }, { - name: "github actions workflow pinned", - args: args{ - r: worklowPinningResult{ - thirdParties: 2, - gitHubOwned: 2, + name: "only GitHub-owned actions pinned", + r: worklowPinningResult{ + gitHubOwned: pinnedResult{ + pinned: 1, + total: 1, + }, + thirdParties: pinnedResult{ + pinned: 0, + total: 1, + }, + }, + scores: []checker.ProportionalScoreWeighted{ + { + Success: 1, + Total: 1, + Weight: 2, + }, + { + Success: 0, + Total: 1, + Weight: 8, }, - dl: &scut.TestDetailLogger{}, }, - want: 0, }, { - name: "error in github actions workflow pinned", - args: args{ - r: worklowPinningResult{ - thirdParties: 2, - gitHubOwned: 2, + name: "only Third-Party actions pinned", + r: worklowPinningResult{ + gitHubOwned: pinnedResult{ + pinned: 0, + total: 1, + }, + thirdParties: pinnedResult{ + pinned: 1, + total: 1, + }, + }, + scores: []checker.ProportionalScoreWeighted{ + { + Success: 0, + Total: 1, + Weight: 2, + }, + { + Success: 1, + Total: 1, + Weight: 8, + }, + }, + }, + { + name: "no GitHub actions pinned", + r: worklowPinningResult{ + gitHubOwned: pinnedResult{ + pinned: 0, + total: 1, + }, + thirdParties: pinnedResult{ + pinned: 0, + total: 1, + }, + }, + scores: []checker.ProportionalScoreWeighted{ + { + Success: 0, + Total: 1, + Weight: 2, + }, + { + Success: 0, + Total: 1, + Weight: 8, + }, + }, + }, + { + name: "no GitHub-owned actions and Third-party actions unpinned", + r: worklowPinningResult{ + gitHubOwned: pinnedResult{ + pinned: 0, + total: 0, + }, + thirdParties: pinnedResult{ + pinned: 0, + total: 1, + }, + }, + scores: []checker.ProportionalScoreWeighted{ + { + Success: 0, + Total: 1, + Weight: 10, + }, + }, + }, + { + name: "no Third-party actions and GitHub-owned actions unpinned", + r: worklowPinningResult{ + gitHubOwned: pinnedResult{ + pinned: 0, + total: 1, + }, + thirdParties: pinnedResult{ + pinned: 0, + total: 0, + }, + }, + scores: []checker.ProportionalScoreWeighted{ + { + Success: 0, + Total: 1, + Weight: 10, + }, + }, + }, + { + name: "no GitHub-owned actions and Third-party actions pinned", + r: worklowPinningResult{ + gitHubOwned: pinnedResult{ + pinned: 0, + total: 0, + }, + thirdParties: pinnedResult{ + pinned: 1, + total: 1, + }, + }, + scores: []checker.ProportionalScoreWeighted{ + { + Success: 1, + Total: 1, + Weight: 10, + }, + }, + }, + { + name: "no Third-party actions and GitHub-owned actions pinned", + r: worklowPinningResult{ + gitHubOwned: pinnedResult{ + pinned: 1, + total: 1, + }, + thirdParties: pinnedResult{ + pinned: 0, + total: 0, + }, + }, + scores: []checker.ProportionalScoreWeighted{ + { + Success: 1, + Total: 1, + Weight: 10, }, - dl: &scut.TestDetailLogger{}, }, - want: 0, }, } for _, tt := range tests { tt := tt // Re-initializing variable so it is not changed while executing the closure below t.Run(tt.name, func(t *testing.T) { t.Parallel() - got, err := createReturnValuesForGitHubActionsWorkflowPinned(tt.args.r, tt.args.infoMsg, tt.args.dl) - if err != nil { - t.Errorf("error during createReturnValuesForGitHubActionsWorkflowPinned: %v", err) - } - if got != tt.want { - t.Errorf("createReturnValuesForGitHubActionsWorkflowPinned() = %v, want %v", got, tt.want) + dl := scut.TestDetailLogger{} + actual := createScoreForGitHubActionsWorkflow(&tt.r, &dl) + diff := cmp.Diff(tt.scores, actual) + if diff != "" { + t.Errorf("createScoreForGitHubActionsWorkflow (-want,+got) %+v", diff) } }) } @@ -90,6 +231,10 @@ func asPointer(s string) *string { return &s } +func asBoolPointer(b bool) *bool { + return &b +} + func Test_PinningDependencies(t *testing.T) { t.Parallel() @@ -99,140 +244,319 @@ func Test_PinningDependencies(t *testing.T) { expected scut.TestReturn }{ { - name: "download then run pinned debug", + name: "all dependencies pinned", dependencies: []checker.Dependency{ + { + Location: &checker.File{ + Snippet: "actions/checkout@a81bbbf8298c0fa03ea29cdc473d45769f953675", + }, + Type: checker.DependencyUseTypeGHAction, + Pinned: asBoolPointer(true), + }, + { + Location: &checker.File{ + Snippet: "other/checkout@a81bbbf8298c0fa03ea29cdc473d45769f953675", + }, + Type: checker.DependencyUseTypeGHAction, + Pinned: asBoolPointer(true), + }, + { + Location: &checker.File{}, + Type: checker.DependencyUseTypeDockerfileContainerImage, + Pinned: asBoolPointer(true), + }, { Location: &checker.File{}, - Msg: asPointer("some message"), Type: checker.DependencyUseTypeDownloadThenRun, + Pinned: asBoolPointer(true), + }, + { + Location: &checker.File{}, + Type: checker.DependencyUseTypeGoCommand, + Pinned: asBoolPointer(true), + }, + { + Location: &checker.File{}, + Type: checker.DependencyUseTypeNpmCommand, + Pinned: asBoolPointer(true), + }, + { + Location: &checker.File{}, + Type: checker.DependencyUseTypePipCommand, + Pinned: asBoolPointer(true), }, }, expected: scut.TestReturn{ Error: nil, - Score: checker.MaxResultScore, + Score: 10, NumberOfWarn: 0, - NumberOfInfo: 8, - NumberOfDebug: 1, + NumberOfInfo: 7, + NumberOfDebug: 0, }, }, { - name: "download then run pinned debug and warn", + name: "all dependencies unpinned", dependencies: []checker.Dependency{ + { + Location: &checker.File{ + Snippet: "actions/checkout@v2", + }, + Type: checker.DependencyUseTypeGHAction, + Pinned: asBoolPointer(false), + }, + { + Location: &checker.File{ + Snippet: "other/checkout@v2", + }, + Type: checker.DependencyUseTypeGHAction, + Pinned: asBoolPointer(false), + }, { Location: &checker.File{}, - Msg: asPointer("some message"), - Type: checker.DependencyUseTypeDownloadThenRun, + Type: checker.DependencyUseTypeDockerfileContainerImage, + Pinned: asBoolPointer(false), }, { Location: &checker.File{}, Type: checker.DependencyUseTypeDownloadThenRun, + Pinned: asBoolPointer(false), + }, + { + Location: &checker.File{}, + Type: checker.DependencyUseTypeGoCommand, + Pinned: asBoolPointer(false), + }, + { + Location: &checker.File{}, + Type: checker.DependencyUseTypeNpmCommand, + Pinned: asBoolPointer(false), + }, + { + Location: &checker.File{}, + Type: checker.DependencyUseTypePipCommand, + Pinned: asBoolPointer(false), }, }, expected: scut.TestReturn{ Error: nil, - Score: 7, - NumberOfWarn: 1, - NumberOfInfo: 6, - NumberOfDebug: 1, + Score: 0, + NumberOfWarn: 7, + NumberOfInfo: 7, + NumberOfDebug: 0, }, }, { - name: "various warnings", + name: "1 ecosystem pinned and 1 ecosystem unpinned", dependencies: []checker.Dependency{ { Location: &checker.File{}, Type: checker.DependencyUseTypePipCommand, + Pinned: asBoolPointer(false), }, { Location: &checker.File{}, - Type: checker.DependencyUseTypeDownloadThenRun, + Type: checker.DependencyUseTypeGoCommand, + Pinned: asBoolPointer(true), }, + }, + expected: scut.TestReturn{ + Error: nil, + Score: 5, + NumberOfWarn: 1, + NumberOfInfo: 2, + NumberOfDebug: 0, + }, + }, + { + name: "1 ecosystem partially pinned", + dependencies: []checker.Dependency{ { Location: &checker.File{}, - Type: checker.DependencyUseTypeDockerfileContainerImage, + Type: checker.DependencyUseTypePipCommand, + Pinned: asBoolPointer(false), }, { Location: &checker.File{}, - Msg: asPointer("debug message"), + Type: checker.DependencyUseTypePipCommand, + Pinned: asBoolPointer(true), }, }, expected: scut.TestReturn{ Error: nil, - Score: 4, - NumberOfWarn: 3, - NumberOfInfo: 4, - NumberOfDebug: 1, + Score: 5, + NumberOfWarn: 1, + NumberOfInfo: 1, + NumberOfDebug: 0, }, }, { - name: "unpinned pip install", + name: "no dependencies found", + dependencies: []checker.Dependency{}, + expected: scut.TestReturn{ + Error: nil, + Score: -1, + NumberOfWarn: 0, + NumberOfInfo: 0, + NumberOfDebug: 0, + }, + }, + { + name: "pinned dependency shows no warn message", dependencies: []checker.Dependency{ { Location: &checker.File{}, Type: checker.DependencyUseTypePipCommand, + Pinned: asBoolPointer(true), }, }, expected: scut.TestReturn{ Error: nil, - Score: 8, + Score: 10, + NumberOfWarn: 0, + NumberOfInfo: 1, + NumberOfDebug: 0, + }, + }, + { + name: "unpinned dependency shows warn message", + dependencies: []checker.Dependency{ + { + Location: &checker.File{}, + Type: checker.DependencyUseTypePipCommand, + Pinned: asBoolPointer(false), + }, + }, + expected: scut.TestReturn{ + Error: nil, + Score: 0, NumberOfWarn: 1, - NumberOfInfo: 7, + NumberOfInfo: 1, NumberOfDebug: 0, }, }, { - name: "undefined pip install", + name: "dependency with parsing error does not count for score and shows debug message", dependencies: []checker.Dependency{ { Location: &checker.File{}, + Msg: asPointer("some message"), Type: checker.DependencyUseTypePipCommand, - Msg: asPointer("debug message"), }, }, expected: scut.TestReturn{ Error: nil, - Score: 10, + Score: -1, NumberOfWarn: 0, - NumberOfInfo: 8, + NumberOfInfo: 0, NumberOfDebug: 1, }, }, { - name: "all dependencies pinned", + name: "dependency missing Pinned info does not count for score and shows debug message", + dependencies: []checker.Dependency{ + { + Location: &checker.File{}, + Type: checker.DependencyUseTypePipCommand, + }, + }, expected: scut.TestReturn{ Error: nil, - Score: 10, + Score: -1, + NumberOfWarn: 0, + NumberOfInfo: 0, + NumberOfDebug: 1, + }, + }, + { + name: "dependency missing Location info and no error message throws error", + dependencies: []checker.Dependency{{}}, + expected: scut.TestReturn{ + Error: sce.ErrScorecardInternal, + Score: -1, NumberOfWarn: 0, - NumberOfInfo: 8, + NumberOfInfo: 0, NumberOfDebug: 0, }, }, { - name: "Validate various warnings and info", + name: "dependency missing Location info with error message shows debug message", + dependencies: []checker.Dependency{{ + Msg: asPointer("some message"), + }}, + expected: scut.TestReturn{ + Error: nil, + Score: -1, + NumberOfWarn: 0, + NumberOfInfo: 0, + NumberOfDebug: 1, + }, + }, + { + name: "unpinned choco install", dependencies: []checker.Dependency{ { Location: &checker.File{}, - Type: checker.DependencyUseTypePipCommand, + Type: checker.DependencyUseTypeChocoCommand, + Pinned: asBoolPointer(false), }, + }, + expected: scut.TestReturn{ + Error: nil, + Score: 0, + NumberOfWarn: 1, + NumberOfInfo: 1, + NumberOfDebug: 0, + }, + }, + { + name: "unpinned Dockerfile container image", + dependencies: []checker.Dependency{ { Location: &checker.File{}, - Type: checker.DependencyUseTypeDownloadThenRun, + Type: checker.DependencyUseTypeDockerfileContainerImage, + Pinned: asBoolPointer(false), }, + }, + expected: scut.TestReturn{ + Error: nil, + Score: 0, + NumberOfWarn: 1, + NumberOfInfo: 1, + NumberOfDebug: 0, + }, + }, + { + name: "unpinned download then run", + dependencies: []checker.Dependency{ { Location: &checker.File{}, - Type: checker.DependencyUseTypeDockerfileContainerImage, + Type: checker.DependencyUseTypeDownloadThenRun, + Pinned: asBoolPointer(false), }, + }, + expected: scut.TestReturn{ + Error: nil, + Score: 0, + NumberOfWarn: 1, + NumberOfInfo: 1, + NumberOfDebug: 0, + }, + }, + { + name: "unpinned go install", + dependencies: []checker.Dependency{ { Location: &checker.File{}, - Msg: asPointer("debug message"), + Type: checker.DependencyUseTypeGoCommand, + Pinned: asBoolPointer(false), }, }, expected: scut.TestReturn{ Error: nil, - Score: 4, - NumberOfWarn: 3, - NumberOfInfo: 4, - NumberOfDebug: 1, + Score: 0, + NumberOfWarn: 1, + NumberOfInfo: 1, + NumberOfDebug: 0, }, }, { @@ -241,177 +565,253 @@ func Test_PinningDependencies(t *testing.T) { { Location: &checker.File{}, Type: checker.DependencyUseTypeNpmCommand, + Pinned: asBoolPointer(false), }, }, expected: scut.TestReturn{ Error: nil, - Score: 8, + Score: 0, NumberOfWarn: 1, - NumberOfInfo: 7, + NumberOfInfo: 1, NumberOfDebug: 0, }, }, { - name: "unpinned go install", + name: "unpinned nuget install", dependencies: []checker.Dependency{ { Location: &checker.File{}, - Type: checker.DependencyUseTypeGoCommand, + Type: checker.DependencyUseTypeNugetCommand, + Pinned: asBoolPointer(false), }, }, expected: scut.TestReturn{ Error: nil, - Score: 8, + Score: 0, NumberOfWarn: 1, - NumberOfInfo: 7, + NumberOfInfo: 1, NumberOfDebug: 0, }, }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - dl := scut.TestDetailLogger{} - c := checker.CheckRequest{Dlogger: &dl} - actual := PinningDependencies("checkname", &c, - &checker.PinningDependenciesData{ - Dependencies: tt.dependencies, - }) - - if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &actual, &dl) { - t.Fail() - } - }) - } -} - -func Test_createReturnValues(t *testing.T) { - t.Parallel() - - type args struct { - pr map[checker.DependencyUseType]pinnedResult - dl *scut.TestDetailLogger - t checker.DependencyUseType - } - - tests := []struct { - name string - args args - want int - }{ { - name: "returns 10 if no error and no pinnedResult", - args: args{ - t: checker.DependencyUseTypeDownloadThenRun, - dl: &scut.TestDetailLogger{}, + name: "unpinned pip install", + dependencies: []checker.Dependency{ + { + Location: &checker.File{}, + Type: checker.DependencyUseTypePipCommand, + Pinned: asBoolPointer(false), + }, + }, + expected: scut.TestReturn{ + Error: nil, + Score: 0, + NumberOfWarn: 1, + NumberOfInfo: 1, + NumberOfDebug: 0, }, - want: 10, }, { - name: "returns 10 if pinned undefined", - args: args{ - t: checker.DependencyUseTypeDownloadThenRun, - pr: map[checker.DependencyUseType]pinnedResult{ - checker.DependencyUseTypeDownloadThenRun: pinnedUndefined, + name: "2 unpinned dependencies for 1 ecosystem shows 2 warn messages", + dependencies: []checker.Dependency{ + { + Location: &checker.File{}, + Type: checker.DependencyUseTypePipCommand, + Pinned: asBoolPointer(false), }, - dl: &scut.TestDetailLogger{}, + { + Location: &checker.File{}, + Type: checker.DependencyUseTypePipCommand, + Pinned: asBoolPointer(false), + }, + }, + expected: scut.TestReturn{ + Error: nil, + Score: 0, + NumberOfWarn: 2, + NumberOfInfo: 1, + NumberOfDebug: 0, }, - want: 10, }, { - name: "returns 10 if pinned", - args: args{ - t: checker.DependencyUseTypeDownloadThenRun, - pr: map[checker.DependencyUseType]pinnedResult{ - checker.DependencyUseTypeDownloadThenRun: pinned, + name: "2 unpinned dependencies for 2 ecosystems shows 2 warn messages", + dependencies: []checker.Dependency{ + { + Location: &checker.File{}, + Type: checker.DependencyUseTypePipCommand, + Pinned: asBoolPointer(false), + }, + { + Location: &checker.File{}, + Type: checker.DependencyUseTypeGoCommand, + Pinned: asBoolPointer(false), }, - dl: &scut.TestDetailLogger{}, }, - want: 10, + expected: scut.TestReturn{ + Error: nil, + Score: 0, + NumberOfWarn: 2, + NumberOfInfo: 2, + NumberOfDebug: 0, + }, }, { - name: "returns 0 if unpinned", - args: args{ - t: checker.DependencyUseTypeDownloadThenRun, - pr: map[checker.DependencyUseType]pinnedResult{ - checker.DependencyUseTypeDownloadThenRun: notPinned, + name: "GitHub Actions ecosystem with GitHub-owned pinned", + dependencies: []checker.Dependency{ + { + Location: &checker.File{ + Snippet: "actions/checkout@a81bbbf8298c0fa03ea29cdc473d45769f953675", + }, + Type: checker.DependencyUseTypeGHAction, + Pinned: asBoolPointer(true), }, - dl: &scut.TestDetailLogger{}, }, - want: 0, + expected: scut.TestReturn{ + Error: nil, + Score: 10, + NumberOfWarn: 0, + NumberOfInfo: 1, + NumberOfDebug: 0, + }, }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got, err := createReturnValues(tt.args.pr, tt.args.t, "some message", tt.args.dl) - if err != nil { - t.Errorf("error during createReturnValues: %v", err) - } - if got != tt.want { - t.Errorf("createReturnValues() = %v, want %v", got, tt.want) - } - - if tt.want < 10 { - return - } - - isExpectedLog := func(logMessage checker.LogMessage, logType checker.DetailType) bool { - return logMessage.Text == "some message" && logType == checker.DetailInfo - } - if !scut.ValidateLogMessage(isExpectedLog, tt.args.dl) { - t.Errorf("test failed: log message not present: %+v", "some message") - } - }) - } -} - -func Test_maxScore(t *testing.T) { - t.Parallel() - type args struct { - s1 int - s2 int - } - tests := []struct { - name string - args args - want int - }{ { - name: "returns s1 if s1 is greater than s2", - args: args{ - s1: 10, - s2: 5, + name: "GitHub Actions ecosystem with third-party pinned", + dependencies: []checker.Dependency{ + { + Location: &checker.File{ + Snippet: "other/checkout@a81bbbf8298c0fa03ea29cdc473d45769f953675", + }, + Type: checker.DependencyUseTypeGHAction, + Pinned: asBoolPointer(true), + }, + }, + expected: scut.TestReturn{ + Error: nil, + Score: 10, + NumberOfWarn: 0, + NumberOfInfo: 1, + NumberOfDebug: 0, }, - want: 10, }, { - name: "returns s2 if s2 is greater than s1", - args: args{ - s1: 5, - s2: 10, + name: "GitHub Actions ecosystem with GitHub-owned and third-party pinned", + dependencies: []checker.Dependency{ + { + Location: &checker.File{ + Snippet: "actions/checkout@a81bbbf8298c0fa03ea29cdc473d45769f953675", + }, + Type: checker.DependencyUseTypeGHAction, + Pinned: asBoolPointer(true), + }, + { + Location: &checker.File{ + Snippet: "other/checkout@a81bbbf8298c0fa03ea29cdc473d45769f953675", + }, + Type: checker.DependencyUseTypeGHAction, + Pinned: asBoolPointer(true), + }, + }, + expected: scut.TestReturn{ + Error: nil, + Score: 10, + NumberOfWarn: 0, + NumberOfInfo: 2, + NumberOfDebug: 0, }, - want: 10, }, { - name: "returns s1 if s1 is equal to s2", - args: args{ - s1: 10, - s2: 10, + name: "GitHub Actions ecosystem with GitHub-owned and third-party unpinned", + dependencies: []checker.Dependency{ + { + Location: &checker.File{ + Snippet: "actions/checkout@v2", + }, + Type: checker.DependencyUseTypeGHAction, + Pinned: asBoolPointer(false), + }, + { + Location: &checker.File{ + Snippet: "other/checkout@v2", + }, + Type: checker.DependencyUseTypeGHAction, + Pinned: asBoolPointer(false), + }, + }, + expected: scut.TestReturn{ + Error: nil, + Score: 0, + NumberOfWarn: 2, + NumberOfInfo: 2, + NumberOfDebug: 0, + }, + }, + { + name: "GitHub Actions ecosystem with GitHub-owned pinned and third-party unpinned", + dependencies: []checker.Dependency{ + { + Location: &checker.File{ + Snippet: "actions/checkout@a81bbbf8298c0fa03ea29cdc473d45769f953675", + }, + Type: checker.DependencyUseTypeGHAction, + Pinned: asBoolPointer(true), + }, + { + Location: &checker.File{ + Snippet: "other/checkout@v2", + }, + Type: checker.DependencyUseTypeGHAction, + Pinned: asBoolPointer(false), + }, + }, + expected: scut.TestReturn{ + Error: nil, + Score: 2, + NumberOfWarn: 1, + NumberOfInfo: 2, + NumberOfDebug: 0, + }, + }, + { + name: "GitHub Actions ecosystem with GitHub-owned unpinned and third-party pinned", + dependencies: []checker.Dependency{ + { + Location: &checker.File{ + Snippet: "actions/checkout@v2", + }, + Type: checker.DependencyUseTypeGHAction, + Pinned: asBoolPointer(false), + }, + { + Location: &checker.File{ + Snippet: "other/checkout@a81bbbf8298c0fa03ea29cdc473d45769f953675", + }, + Type: checker.DependencyUseTypeGHAction, + Pinned: asBoolPointer(true), + }, + }, + expected: scut.TestReturn{ + Error: nil, + Score: 8, + NumberOfWarn: 1, + NumberOfInfo: 2, + NumberOfDebug: 0, }, - want: 10, }, } + for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - if got := maxScore(tt.args.s1, tt.args.s2); got != tt.want { - t.Errorf("maxScore() = %v, want %v", got, tt.want) + + dl := scut.TestDetailLogger{} + c := checker.CheckRequest{Dlogger: &dl} + actual := PinningDependencies("checkname", &c, + &checker.PinningDependenciesData{ + Dependencies: tt.dependencies, + }) + + if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &actual, &dl) { + t.Fail() } }) } @@ -427,12 +827,12 @@ func Test_generateOwnerToDisplay(t *testing.T) { { name: "returns GitHub if gitHubOwned is true", gitHubOwned: true, - want: "GitHub-owned", + want: "GitHub-owned GitHubAction", }, { name: "returns GitHub if gitHubOwned is false", gitHubOwned: false, - want: "third-party", + want: "third-party GitHubAction", }, } for _, tt := range tests { @@ -449,50 +849,112 @@ func Test_generateOwnerToDisplay(t *testing.T) { func Test_addWorkflowPinnedResult(t *testing.T) { t.Parallel() type args struct { - w *worklowPinningResult - to bool - isGitHub bool + dependency *checker.Dependency + w *worklowPinningResult + isGitHub bool } tests := []struct { //nolint:govet name string - want pinnedResult + want *worklowPinningResult args args }{ { - name: "sets pinned to true if to is true", + name: "add pinned GitHub-owned action dependency", args: args{ + dependency: &checker.Dependency{ + Pinned: asBoolPointer(true), + }, w: &worklowPinningResult{}, - to: true, isGitHub: true, }, - want: pinned, + want: &worklowPinningResult{ + thirdParties: pinnedResult{ + pinned: 0, + total: 0, + }, + gitHubOwned: pinnedResult{ + pinned: 1, + total: 1, + }, + }, }, { - name: "sets pinned to false if to is false", + name: "add unpinned GitHub-owned action dependency", args: args{ + dependency: &checker.Dependency{ + Pinned: asBoolPointer(false), + }, w: &worklowPinningResult{}, - to: false, isGitHub: true, }, - want: notPinned, + want: &worklowPinningResult{ + thirdParties: pinnedResult{ + pinned: 0, + total: 0, + }, + gitHubOwned: pinnedResult{ + pinned: 0, + total: 1, + }, + }, + }, + { + name: "add pinned Third-Party action dependency", + args: args{ + dependency: &checker.Dependency{ + Pinned: asBoolPointer(true), + }, + w: &worklowPinningResult{}, + isGitHub: false, + }, + want: &worklowPinningResult{ + thirdParties: pinnedResult{ + pinned: 1, + total: 1, + }, + gitHubOwned: pinnedResult{ + pinned: 0, + total: 0, + }, + }, }, { - name: "sets pinned to undefined if to is false and isGitHub is false", + name: "add unpinned Third-Party action dependency", args: args{ + dependency: &checker.Dependency{ + Pinned: asBoolPointer(false), + }, w: &worklowPinningResult{}, - to: false, isGitHub: false, }, - want: pinnedUndefined, + want: &worklowPinningResult{ + thirdParties: pinnedResult{ + pinned: 0, + total: 1, + }, + gitHubOwned: pinnedResult{ + pinned: 0, + total: 0, + }, + }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - addWorkflowPinnedResult(tt.args.w, tt.args.to, tt.args.isGitHub) - if tt.args.w.gitHubOwned != tt.want { - t.Errorf("addWorkflowPinnedResult() = %v, want %v", tt.args.w.gitHubOwned, tt.want) + addWorkflowPinnedResult(tt.args.dependency, tt.args.w, tt.args.isGitHub) + if tt.want.thirdParties != tt.args.w.thirdParties { + t.Errorf("addWorkflowPinnedResult Third-party GitHub actions mismatch (-want +got):"+ + "\nThird-party pinned: %s\nThird-party total: %s", + cmp.Diff(tt.want.thirdParties.pinned, tt.args.w.thirdParties.pinned), + cmp.Diff(tt.want.thirdParties.total, tt.args.w.thirdParties.total)) + } + if tt.want.gitHubOwned != tt.args.w.gitHubOwned { + t.Errorf("addWorkflowPinnedResult GitHub-owned GitHub actions mismatch (-want +got):"+ + "\nGitHub-owned pinned: %s\nGitHub-owned total: %s", + cmp.Diff(tt.want.gitHubOwned.pinned, tt.args.w.gitHubOwned.pinned), + cmp.Diff(tt.want.gitHubOwned.total, tt.args.w.gitHubOwned.total)) } }) } @@ -538,50 +1000,193 @@ func TestGenerateText(t *testing.T) { func TestUpdatePinningResults(t *testing.T) { t.Parallel() + type args struct { + dependency *checker.Dependency + w *worklowPinningResult + pr map[checker.DependencyUseType]pinnedResult + } + type want struct { + w *worklowPinningResult + pr map[checker.DependencyUseType]pinnedResult + } tests := []struct { //nolint:govet - name string - dependency *checker.Dependency - expectedPinningResult *worklowPinningResult - expectedPinnedResult map[checker.DependencyUseType]pinnedResult + name string + args args + want want }{ { - name: "GitHub Action - GitHub-owned", - dependency: &checker.Dependency{ - Type: checker.DependencyUseTypeGHAction, - Location: &checker.File{ - Snippet: "actions/checkout@v2", + name: "add pinned GitHub-owned action", + args: args{ + dependency: &checker.Dependency{ + Type: checker.DependencyUseTypeGHAction, + Location: &checker.File{ + Snippet: "actions/checkout@a81bbbf8298c0fa03ea29cdc473d45769f953675", + }, + Pinned: asBoolPointer(true), }, + w: &worklowPinningResult{}, + pr: make(map[checker.DependencyUseType]pinnedResult), }, - expectedPinningResult: &worklowPinningResult{ - thirdParties: 0, - gitHubOwned: 2, + want: want{ + w: &worklowPinningResult{ + thirdParties: pinnedResult{ + pinned: 0, + total: 0, + }, + gitHubOwned: pinnedResult{ + pinned: 1, + total: 1, + }, + }, + pr: make(map[checker.DependencyUseType]pinnedResult), }, - expectedPinnedResult: map[checker.DependencyUseType]pinnedResult{}, }, { - name: "Third party owned.", - dependency: &checker.Dependency{ - Type: checker.DependencyUseTypeGHAction, - Location: &checker.File{ - Snippet: "other/checkout@v2", + name: "add unpinned GitHub-owned action", + args: args{ + dependency: &checker.Dependency{ + Type: checker.DependencyUseTypeGHAction, + Location: &checker.File{ + Snippet: "actions/checkout@v2", + }, + Pinned: asBoolPointer(false), }, + w: &worklowPinningResult{}, + pr: make(map[checker.DependencyUseType]pinnedResult), }, - expectedPinningResult: &worklowPinningResult{ - thirdParties: 2, - gitHubOwned: 0, + want: want{ + w: &worklowPinningResult{ + thirdParties: pinnedResult{ + pinned: 0, + total: 0, + }, + gitHubOwned: pinnedResult{ + pinned: 0, + total: 1, + }, + }, + pr: make(map[checker.DependencyUseType]pinnedResult), + }, + }, + { + name: "add pinned Third-party action", + args: args{ + dependency: &checker.Dependency{ + Type: checker.DependencyUseTypeGHAction, + Location: &checker.File{ + Snippet: "other/checkout@ffa6706ff2127a749973072756f83c532e43ed02", + }, + Pinned: asBoolPointer(true), + }, + w: &worklowPinningResult{}, + pr: make(map[checker.DependencyUseType]pinnedResult), + }, + want: want{ + w: &worklowPinningResult{ + thirdParties: pinnedResult{ + pinned: 1, + total: 1, + }, + gitHubOwned: pinnedResult{ + pinned: 0, + total: 0, + }, + }, + pr: make(map[checker.DependencyUseType]pinnedResult), + }, + }, + { + name: "add unpinned Third-party action", + args: args{ + dependency: &checker.Dependency{ + Type: checker.DependencyUseTypeGHAction, + Location: &checker.File{ + Snippet: "other/checkout@v2", + }, + Pinned: asBoolPointer(false), + }, + w: &worklowPinningResult{}, + pr: make(map[checker.DependencyUseType]pinnedResult), + }, + want: want{ + w: &worklowPinningResult{ + thirdParties: pinnedResult{ + pinned: 0, + total: 1, + }, + gitHubOwned: pinnedResult{ + pinned: 0, + total: 0, + }, + }, + pr: make(map[checker.DependencyUseType]pinnedResult), + }, + }, + { + name: "add pinned pip install", + args: args{ + dependency: &checker.Dependency{ + Type: checker.DependencyUseTypePipCommand, + Pinned: asBoolPointer(true), + }, + w: &worklowPinningResult{}, + pr: make(map[checker.DependencyUseType]pinnedResult), + }, + want: want{ + w: &worklowPinningResult{}, + pr: map[checker.DependencyUseType]pinnedResult{ + checker.DependencyUseTypePipCommand: { + pinned: 1, + total: 1, + }, + }, + }, + }, + { + name: "add unpinned pip install", + args: args{ + dependency: &checker.Dependency{ + Type: checker.DependencyUseTypePipCommand, + Pinned: asBoolPointer(false), + }, + w: &worklowPinningResult{}, + pr: make(map[checker.DependencyUseType]pinnedResult), + }, + want: want{ + w: &worklowPinningResult{}, + pr: map[checker.DependencyUseType]pinnedResult{ + checker.DependencyUseTypePipCommand: { + pinned: 0, + total: 1, + }, + }, }, - expectedPinnedResult: map[checker.DependencyUseType]pinnedResult{}, }, } for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - wp := &worklowPinningResult{} - pr := make(map[checker.DependencyUseType]pinnedResult) - updatePinningResults(tc.dependency, wp, pr) - if tc.expectedPinningResult.thirdParties != wp.thirdParties && tc.expectedPinningResult.gitHubOwned != wp.gitHubOwned { //nolint:lll - t.Errorf("updatePinningResults mismatch (-want +got):\n%s", cmp.Diff(tc.expectedPinningResult, wp)) + updatePinningResults(tc.args.dependency, tc.args.w, tc.args.pr) + if tc.want.w.thirdParties != tc.args.w.thirdParties { + t.Errorf("updatePinningResults Third-party GitHub actions mismatch (-want +got):"+ + "\nThird-party pinned: %s\nThird-party total: %s", + cmp.Diff(tc.want.w.thirdParties.pinned, tc.args.w.thirdParties.pinned), + cmp.Diff(tc.want.w.thirdParties.total, tc.args.w.thirdParties.total)) + } + if tc.want.w.gitHubOwned != tc.args.w.gitHubOwned { + t.Errorf("updatePinningResults GitHub-owned GitHub actions mismatch (-want +got):"+ + "\nGitHub-owned pinned: %s\nGitHub-owned total: %s", + cmp.Diff(tc.want.w.gitHubOwned.pinned, tc.args.w.gitHubOwned.pinned), + cmp.Diff(tc.want.w.gitHubOwned.total, tc.args.w.gitHubOwned.total)) + } + for dependencyUseType := range tc.want.pr { + if tc.want.pr[dependencyUseType] != tc.args.pr[dependencyUseType] { + t.Errorf("updatePinningResults %s mismatch (-want +got):\npinned: %s\ntotal: %s", + dependencyUseType, + cmp.Diff(tc.want.pr[dependencyUseType].pinned, tc.args.pr[dependencyUseType].pinned), + cmp.Diff(tc.want.pr[dependencyUseType].total, tc.args.pr[dependencyUseType].total)) + } } }) } diff --git a/checks/fileparser/github_workflow.go b/checks/fileparser/github_workflow.go index cc1622d0654..1f970a4c0e0 100644 --- a/checks/fileparser/github_workflow.go +++ b/checks/fileparser/github_workflow.go @@ -131,6 +131,11 @@ func getJobDefaultRunShell(job *actionlint.Job) string { func getJobRunsOnLabels(job *actionlint.Job) []*actionlint.String { if job != nil && job.RunsOn != nil { + // Starting at v1.6.16, either field may be set + // https://github.com/rhysd/actionlint/issues/164 + if job.RunsOn.LabelsExpr != nil { + return []*actionlint.String{job.RunsOn.LabelsExpr} + } return job.RunsOn.Labels } return nil @@ -235,7 +240,7 @@ func GetShellForStep(step *actionlint.Step, job *actionlint.Job) (string, error) } execRunShell := getExecRunShell(execRun) if execRunShell != "" { - return execRun.Shell.Value, nil + return execRunShell, nil } jobDefaultRunShell := getJobDefaultRunShell(job) if jobDefaultRunShell != "" { diff --git a/checks/fileparser/github_workflow_test.go b/checks/fileparser/github_workflow_test.go index cf2912e4fc2..2c809467ac2 100644 --- a/checks/fileparser/github_workflow_test.go +++ b/checks/fileparser/github_workflow_test.go @@ -20,8 +20,8 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" "github.com/rhysd/actionlint" - "gotest.tools/assert/cmp" ) func TestGitHubWorkflowShell(t *testing.T) { @@ -142,7 +142,7 @@ func TestGitHubWorkflowShell(t *testing.T) { actualShells = append(actualShells, shell) } } - if !cmp.DeepEqual(tt.expectedShells, actualShells)().Success() { + if !cmp.Equal(tt.expectedShells, actualShells) { t.Errorf("%v: Got (%v) expected (%v)", tt.name, actualShells, tt.expectedShells) } }) diff --git a/checks/permissions_test.go b/checks/permissions_test.go index 0b06e0628f8..5ba90f2545e 100644 --- a/checks/permissions_test.go +++ b/checks/permissions_test.go @@ -280,7 +280,7 @@ func TestGithubTokenPermissions(t *testing.T) { }, }, { - name: "release workflow contents write semantic-release", + name: "release workflow contents write semantic-release with npx", filenames: []string{"./testdata/.github/workflows/github-workflow-permissions-contents-writes-release-semantic-release.yaml"}, expected: scut.TestReturn{ Error: nil, @@ -290,6 +290,28 @@ func TestGithubTokenPermissions(t *testing.T) { NumberOfDebug: 4, }, }, + { + name: "release workflow contents write semantic-release with yarn command", + filenames: []string{"./testdata/.github/workflows/github-workflow-permissions-contents-writes-release-semantic-release-yarn.yaml"}, + expected: scut.TestReturn{ + Error: nil, + Score: checker.MaxResultScore, + NumberOfWarn: 0, + NumberOfInfo: 2, + NumberOfDebug: 4, + }, + }, + { + name: "release workflow contents write semantic-release with pnpm and dlx", + filenames: []string{"./testdata/.github/workflows/github-workflow-permissions-contents-writes-release-semantic-release-pnpm.yaml"}, + expected: scut.TestReturn{ + Error: nil, + Score: checker.MaxResultScore, + NumberOfWarn: 0, + NumberOfInfo: 2, + NumberOfDebug: 4, + }, + }, { name: "package workflow write", filenames: []string{"./testdata/.github/workflows/github-workflow-permissions-packages-writes.yaml"}, diff --git a/checks/raw/dependency_update_tool.go b/checks/raw/dependency_update_tool.go index 3af013b4088..2e2244c1b8b 100644 --- a/checks/raw/dependency_update_tool.go +++ b/checks/raw/dependency_update_tool.go @@ -136,3 +136,7 @@ var checkDependencyFileExists fileparser.DoWhileTrueOnFilename = func(name strin func asPointer(s string) *string { return &s } + +func asBoolPointer(b bool) *bool { + return &b +} diff --git a/checks/raw/permissions.go b/checks/raw/permissions.go index c80e45fdeab..540fe4be223 100644 --- a/checks/raw/permissions.go +++ b/checks/raw/permissions.go @@ -473,7 +473,7 @@ func isReleasingWorkflow(workflow *actionlint.Workflow, fp string, pdata *permis // Commonly JavaScript packages, but supports multiple ecosystems Steps: []*fileparser.JobMatcherStep{ { - Run: "npx.*semantic-release", + Run: "(npx|pnpm|yarn).*semantic-release", }, }, LogText: "candidate publishing workflow using semantic-release", diff --git a/checks/raw/pinned_dependencies.go b/checks/raw/pinned_dependencies.go index 0b93181443e..90b28bdff8a 100644 --- a/checks/raw/pinned_dependencies.go +++ b/checks/raw/pinned_dependencies.go @@ -261,7 +261,6 @@ var validateDockerfilesPinning fileparser.DoWhileTrueOnFileContent = func( if pinned || regex.MatchString(name) { // Record the asName. pinnedAsNames[asName] = true - continue } pdata.Dependencies = append(pdata.Dependencies, @@ -275,6 +274,7 @@ var validateDockerfilesPinning fileparser.DoWhileTrueOnFileContent = func( }, Name: asPointer(name), PinnedAt: asPointer(asName), + Pinned: asBoolPointer(pinnedAsNames[asName]), Type: checker.DependencyUseTypeDockerfileContainerImage, }, ) @@ -283,27 +283,26 @@ var validateDockerfilesPinning fileparser.DoWhileTrueOnFileContent = func( case len(valueList) == 1: name := valueList[0] pinned := pinnedAsNames[name] - if !pinned && !regex.MatchString(name) { - dep := checker.Dependency{ - Location: &checker.File{ - Path: pathfn, - Type: finding.FileTypeSource, - Offset: uint(child.StartLine), - EndOffset: uint(child.EndLine), - Snippet: child.Original, - }, - Type: checker.DependencyUseTypeDockerfileContainerImage, - } - parts := strings.SplitN(name, ":", 2) - if len(parts) > 0 { - dep.Name = asPointer(parts[0]) - if len(parts) > 1 { - dep.PinnedAt = asPointer(parts[1]) - } + + dep := checker.Dependency{ + Location: &checker.File{ + Path: pathfn, + Type: finding.FileTypeSource, + Offset: uint(child.StartLine), + EndOffset: uint(child.EndLine), + Snippet: child.Original, + }, + Pinned: asBoolPointer(pinned || regex.MatchString(name)), + Type: checker.DependencyUseTypeDockerfileContainerImage, + } + parts := strings.SplitN(name, ":", 2) + if len(parts) > 0 { + dep.Name = asPointer(parts[0]) + if len(parts) > 1 { + dep.PinnedAt = asPointer(parts[1]) } - pdata.Dependencies = append(pdata.Dependencies, dep) } - + pdata.Dependencies = append(pdata.Dependencies, dep) default: // That should not happen. return false, sce.WithMessage(sce.ErrScorecardInternal, errInternalInvalidDockerFile.Error()) @@ -470,26 +469,25 @@ var validateGitHubActionWorkflow fileparser.DoWhileTrueOnFileContent = func( continue } - if !isActionDependencyPinned(execAction.Uses.Value) { - dep := checker.Dependency{ - Location: &checker.File{ - Path: pathfn, - Type: finding.FileTypeSource, - Offset: uint(execAction.Uses.Pos.Line), - EndOffset: uint(execAction.Uses.Pos.Line), // `Uses` always span a single line. - Snippet: execAction.Uses.Value, - }, - Type: checker.DependencyUseTypeGHAction, - } - parts := strings.SplitN(execAction.Uses.Value, "@", 2) - if len(parts) > 0 { - dep.Name = asPointer(parts[0]) - if len(parts) > 1 { - dep.PinnedAt = asPointer(parts[1]) - } + dep := checker.Dependency{ + Location: &checker.File{ + Path: pathfn, + Type: finding.FileTypeSource, + Offset: uint(execAction.Uses.Pos.Line), + EndOffset: uint(execAction.Uses.Pos.Line), // `Uses` always span a single line. + Snippet: execAction.Uses.Value, + }, + Pinned: asBoolPointer(isActionDependencyPinned(execAction.Uses.Value)), + Type: checker.DependencyUseTypeGHAction, + } + parts := strings.SplitN(execAction.Uses.Value, "@", 2) + if len(parts) > 0 { + dep.Name = asPointer(parts[0]) + if len(parts) > 1 { + dep.PinnedAt = asPointer(parts[1]) } - pdata.Dependencies = append(pdata.Dependencies, dep) } + pdata.Dependencies = append(pdata.Dependencies, dep) } } diff --git a/checks/raw/pinned_dependencies_test.go b/checks/raw/pinned_dependencies_test.go index d1bf323d6c9..bd0e804ff31 100644 --- a/checks/raw/pinned_dependencies_test.go +++ b/checks/raw/pinned_dependencies_test.go @@ -92,8 +92,10 @@ func TestGithubWorkflowPinning(t *testing.T) { return } - if tt.warns != len(r.Dependencies) { - t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + unpinned := countUnpinned(r.Dependencies) + + if tt.warns != unpinned { + t.Errorf("expected %v. Got %v", tt.warns, unpinned) } }) } @@ -241,8 +243,10 @@ func TestNonGithubWorkflowPinning(t *testing.T) { return } - if tt.warns != len(r.Dependencies) { - t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + unpinned := countUnpinned(r.Dependencies) + + if tt.warns != unpinned { + t.Errorf("expected %v. Got %v", tt.warns, unpinned) } }) } @@ -288,8 +292,10 @@ func TestGithubWorkflowPkgManagerPinning(t *testing.T) { return } - if tt.warns != len(r.Dependencies) { - t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + unpinned := countUnpinned(r.Dependencies) + + if tt.warns != unpinned { + t.Errorf("expected %v. Got %v", tt.warns, unpinned) } }) } @@ -365,8 +371,10 @@ func TestDockerfilePinning(t *testing.T) { return } - if tt.warns != len(r.Dependencies) { - t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + unpinned := countUnpinned(r.Dependencies) + + if tt.warns != unpinned { + t.Errorf("expected %v. Got %v", tt.warns, unpinned) } }) } @@ -919,8 +927,10 @@ func TestDockerfilePinningWihoutHash(t *testing.T) { return } - if tt.warns != len(r.Dependencies) { - t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + unpinned := countUnpinned(r.Dependencies) + + if tt.warns != unpinned { + t.Errorf("expected %v. Got %v", tt.warns, unpinned) } }) } @@ -1022,8 +1032,10 @@ func TestDockerfileScriptDownload(t *testing.T) { return } - if tt.warns != len(r.Dependencies) { - t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + unpinned := countUnpinned(r.Dependencies) + + if tt.warns != unpinned { + t.Errorf("expected %v. Got %v", tt.warns, unpinned) } }) } @@ -1064,8 +1076,10 @@ func TestDockerfileScriptDownloadInfo(t *testing.T) { return } - if tt.warns != len(r.Dependencies) { - t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + unpinned := countUnpinned(r.Dependencies) + + if tt.warns != unpinned { + t.Errorf("expected %v. Got %v", tt.warns, unpinned) } }) } @@ -1140,12 +1154,14 @@ func TestShellScriptDownload(t *testing.T) { return } + unpinned := countUnpinned(r.Dependencies) + // Note: this works because all our examples // either have warns or debugs. - ws := (tt.warns == len(r.Dependencies)) && (tt.debugs == 0) - ds := (tt.debugs == len(r.Dependencies)) && (tt.warns == 0) + ws := (tt.warns == unpinned) && (tt.debugs == 0) + ds := (tt.debugs == unpinned) && (tt.warns == 0) if !ws && !ds { - t.Errorf("expected %v or %v. Got %v", tt.warns, tt.debugs, len(r.Dependencies)) + t.Errorf("expected %v or %v. Got %v", tt.warns, tt.debugs, unpinned) } }) } @@ -1192,8 +1208,10 @@ func TestShellScriptDownloadPinned(t *testing.T) { return } - if tt.warns != len(r.Dependencies) { - t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + unpinned := countUnpinned(r.Dependencies) + + if tt.warns != unpinned { + t.Errorf("expected %v. Got %v", tt.warns, unpinned) } }) } @@ -1251,8 +1269,10 @@ func TestGitHubWorflowRunDownload(t *testing.T) { return } - if tt.warns != len(r.Dependencies) { - t.Errorf("expected %v. Got %v", tt.warns, len(r.Dependencies)) + unpinned := countUnpinned(r.Dependencies) + + if tt.warns != unpinned { + t.Errorf("expected %v. Got %v", tt.warns, unpinned) } }) } @@ -1409,3 +1429,15 @@ func TestGitHubWorkInsecureDownloadsLineNumber(t *testing.T) { }) } } + +func countUnpinned(r []checker.Dependency) int { + var unpinned int + + for _, dependency := range r { + if *dependency.Pinned == false { + unpinned += 1 + } + } + + return unpinned +} diff --git a/checks/raw/shell_download_validate.go b/checks/raw/shell_download_validate.go index 524e6d6e57e..fe7c258c2d5 100644 --- a/checks/raw/shell_download_validate.go +++ b/checks/raw/shell_download_validate.go @@ -337,7 +337,8 @@ func collectFetchPipeExecute(startLine, endLine uint, node syntax.Node, cmd, pat EndOffset: endLine, Snippet: cmd, }, - Type: checker.DependencyUseTypeDownloadThenRun, + Pinned: asBoolPointer(false), + Type: checker.DependencyUseTypeDownloadThenRun, }, ) } @@ -388,7 +389,8 @@ func collectExecuteFiles(startLine, endLine uint, node syntax.Node, cmd, pathfn EndOffset: endLine, Snippet: cmd, }, - Type: checker.DependencyUseTypeDownloadThenRun, + Pinned: asBoolPointer(false), + Type: checker.DependencyUseTypeDownloadThenRun, }, ) } @@ -397,56 +399,50 @@ func collectExecuteFiles(startLine, endLine uint, node syntax.Node, cmd, pathfn // Npm install docs are here. // https://docs.npmjs.com/cli/v7/commands/npm-install -func isNpmUnpinnedDownload(cmd []string) bool { - if len(cmd) == 0 { - return false - } - +func isNpmDownload(cmd []string) bool { if !isBinaryName("npm", cmd[0]) { return false } for i := 1; i < len(cmd); i++ { // Search for get/install/update commands. - // `npm ci` wil verify all hashes are present. if strings.EqualFold(cmd[i], "install") || strings.EqualFold(cmd[i], "i") || strings.EqualFold(cmd[i], "install-test") || - strings.EqualFold(cmd[i], "update") { + strings.EqualFold(cmd[i], "update") || + strings.EqualFold(cmd[i], "ci") { return true } } return false } -func isGoUnpinnedDownload(cmd []string) bool { - if len(cmd) == 0 { - return false +func isNpmUnpinnedDownload(cmd []string) bool { + for i := 1; i < len(cmd); i++ { + // `npm ci` wil verify all hashes are present. + if strings.EqualFold(cmd[i], "ci") { + return false + } } + return true +} - if !isBinaryName("go", cmd[0]) { - return false - } +func isGoDownload(cmd []string) bool { // `Go install` will automatically look up the // go.mod and go.sum, so we don't flag it. if len(cmd) <= 2 { return false } - found := false + return isBinaryName("go", cmd[0]) && slices.Contains([]string{"get", "install"}, cmd[1]) +} + +func isGoUnpinnedDownload(cmd []string) bool { insecure := false hashRegex := regexp.MustCompile("^[A-Fa-f0-9]{40,}$") semverRegex := regexp.MustCompile(`^v\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?(\+[0-9A-Za-z-.]+)?$`) - for i := 1; i < len(cmd)-1; i++ { - // Search for get and install commands. - if slices.Contains([]string{"get", "install"}, cmd[i]) { - found = true - } - - if !found { - continue - } + for i := 1; i < len(cmd)-1; i++ { // Skip all flags // TODO skip other build flags which might take arguments for i < len(cmd)-1 && slices.Contains([]string{"-d", "-f", "-t", "-u", "-v", "-fix", "-insecure"}, cmd[i+1]) { @@ -485,7 +481,15 @@ func isGoUnpinnedDownload(cmd []string) bool { } } - return found + return true +} + +func isPipInstall(cmd []string) bool { + if len(cmd) < 2 { + return false + } + + return (isBinaryName("pip", cmd[0]) || isBinaryName("pip3", cmd[0])) && strings.EqualFold(cmd[1], "install") } func isPinnedEditableSource(pkgSource string) bool { @@ -509,28 +513,13 @@ func isFlag(cmd string) bool { } func isUnpinnedPipInstall(cmd []string) bool { - if !isBinaryName("pip", cmd[0]) && !isBinaryName("pip3", cmd[0]) { - return false - } - - isInstall := false hasNoDeps := false isEditableInstall := false isPinnedEditableInstall := true hasRequireHashes := false hasAdditionalArgs := false hasWheel := false - for i := 1; i < len(cmd); i++ { - // Search for install commands. - if strings.EqualFold(cmd[i], "install") { - isInstall = true - continue - } - - if !isInstall { - break - } - + for i := 2; i < len(cmd); i++ { // Require --no-deps to not install the dependencies when doing editable install // because we can't verify if dependencies are pinned // https://pip.pypa.io/en/stable/topics/secure-installs/#do-not-use-setuptools-directly @@ -609,7 +598,7 @@ func isUnpinnedPipInstall(cmd []string) bool { // Any other form of install is unpinned, // e.g. `pip install`. - return isInstall + return true } func isPythonCommand(cmd []string) bool { @@ -637,49 +626,52 @@ func extractPipCommand(cmd []string) ([]string, bool) { return nil, false } -func isUnpinnedPythonPipInstall(cmd []string) bool { +func isPythonPipInstall(cmd []string) bool { if !isPythonCommand(cmd) { return false } + pipCommand, ok := extractPipCommand(cmd) if !ok { return false } + + return isPipInstall(pipCommand) +} + +func isUnpinnedPythonPipInstall(cmd []string) bool { + pipCommand, _ := extractPipCommand(cmd) return isUnpinnedPipInstall(pipCommand) } -func isPipUnpinnedDownload(cmd []string) bool { - if len(cmd) == 0 { - return false - } +func isPipDownload(cmd []string) bool { + return isPipInstall(cmd) || isPythonPipInstall(cmd) +} - if isUnpinnedPipInstall(cmd) { +func isPipUnpinnedDownload(cmd []string) bool { + if isPipInstall(cmd) && isUnpinnedPipInstall(cmd) { return true } - if isUnpinnedPythonPipInstall(cmd) { + if isPythonPipInstall(cmd) && isUnpinnedPythonPipInstall(cmd) { return true } return false } -func isChocoUnpinnedDownload(cmd []string) bool { +func isChocoDownload(cmd []string) bool { // Install command is in the form 'choco install ...' if len(cmd) < 2 { return false } - if !isBinaryName("choco", cmd[0]) && !isBinaryName("choco.exe", cmd[0]) { - return false - } - - if !strings.EqualFold(cmd[1], "install") { - return false - } + return (isBinaryName("choco", cmd[0]) || isBinaryName("choco.exe", cmd[0])) && strings.EqualFold(cmd[1], "install") +} +func isChocoUnpinnedDownload(cmd []string) bool { // If this is an install command, then some variant of requirechecksum must be present. - for i := 1; i < len(cmd); i++ { + for i := 2; i < len(cmd); i++ { parts := strings.Split(cmd[i], "=") if len(parts) == 0 { continue @@ -697,22 +689,17 @@ func isChocoUnpinnedDownload(cmd []string) bool { return true } -func isUnpinnedNugetCliInstall(cmd []string) bool { +func isNugetCliInstall(cmd []string) bool { // looking for command of type nuget install ... if len(cmd) < 2 { return false } - // Search for nuget commands. - if !isBinaryName("nuget", cmd[0]) && !isBinaryName("nuget.exe", cmd[0]) { - return false - } - - // Search for install commands. - if !strings.EqualFold(cmd[1], "install") { - return false - } + // Search for nuget install commands. + return (isBinaryName("nuget", cmd[0]) || isBinaryName("nuget.exe", cmd[0])) && strings.EqualFold(cmd[1], "install") +} +func isUnpinnedNugetCliInstall(cmd []string) bool { // Assume installing a project with PackageReference (with versions) // or packages.config at the root of command if len(cmd) == 2 { @@ -740,26 +727,19 @@ func isUnpinnedNugetCliInstall(cmd []string) bool { return unpinnedDependency } -func isUnpinnedDotNetCliInstall(cmd []string) bool { +func isDotNetCliInstall(cmd []string) bool { // Search for command of type dotnet add package if len(cmd) < 4 { return false } - // Search for dotnet commands. - if !isBinaryName("dotnet", cmd[0]) && !isBinaryName("dotnet.exe", cmd[0]) { - return false - } - - // Search for add commands. - if !strings.EqualFold(cmd[1], "add") { - return false - } - - // Search for package commands (can be either the second or the third word) - if !(strings.EqualFold(cmd[2], "package") || strings.EqualFold(cmd[3], "package")) { - return false - } + // Search for dotnet add package + // where package command can be either the second or the third word + return (isBinaryName("dotnet", cmd[0]) || isBinaryName("dotnet.exe", cmd[0])) && + strings.EqualFold(cmd[1], "add") && + (strings.EqualFold(cmd[2], "package") || strings.EqualFold(cmd[3], "package")) +} +func isUnpinnedDotNetCliInstall(cmd []string) bool { unpinnedDependency := true for i := 3; i < len(cmd); i++ { // look for version flag @@ -772,12 +752,16 @@ func isUnpinnedDotNetCliInstall(cmd []string) bool { return unpinnedDependency } +func isNugetDownload(cmd []string) bool { + return isDotNetCliInstall(cmd) || isNugetCliInstall(cmd) +} + func isNugetUnpinnedDownload(cmd []string) bool { - if isUnpinnedDotNetCliInstall(cmd) { + if isDotNetCliInstall(cmd) && isUnpinnedDotNetCliInstall(cmd) { return true } - if isUnpinnedNugetCliInstall(cmd) { + if isNugetCliInstall(cmd) && isUnpinnedNugetCliInstall(cmd) { return true } @@ -799,8 +783,12 @@ func collectUnpinnedPakageManagerDownload(startLine, endLine uint, node syntax.N startLine, endLine = getLine(startLine, endLine, node) + if len(c) == 0 { + return + } + // Go get/install. - if isGoUnpinnedDownload(c) { + if isGoDownload(c) { r.Dependencies = append(r.Dependencies, checker.Dependency{ Location: &checker.File{ @@ -810,7 +798,8 @@ func collectUnpinnedPakageManagerDownload(startLine, endLine uint, node syntax.N EndOffset: endLine, Snippet: cmd, }, - Type: checker.DependencyUseTypeGoCommand, + Pinned: asBoolPointer(!isGoUnpinnedDownload(c)), + Type: checker.DependencyUseTypeGoCommand, }, ) @@ -818,7 +807,7 @@ func collectUnpinnedPakageManagerDownload(startLine, endLine uint, node syntax.N } // Pip install. - if isPipUnpinnedDownload(c) { + if isPipDownload(c) { r.Dependencies = append(r.Dependencies, checker.Dependency{ Location: &checker.File{ @@ -828,7 +817,8 @@ func collectUnpinnedPakageManagerDownload(startLine, endLine uint, node syntax.N EndOffset: endLine, Snippet: cmd, }, - Type: checker.DependencyUseTypePipCommand, + Pinned: asBoolPointer(!isPipUnpinnedDownload(c)), + Type: checker.DependencyUseTypePipCommand, }, ) @@ -836,7 +826,7 @@ func collectUnpinnedPakageManagerDownload(startLine, endLine uint, node syntax.N } // Npm install. - if isNpmUnpinnedDownload(c) { + if isNpmDownload(c) { r.Dependencies = append(r.Dependencies, checker.Dependency{ Location: &checker.File{ @@ -846,7 +836,8 @@ func collectUnpinnedPakageManagerDownload(startLine, endLine uint, node syntax.N EndOffset: endLine, Snippet: cmd, }, - Type: checker.DependencyUseTypeNpmCommand, + Pinned: asBoolPointer(!isNpmUnpinnedDownload(c)), + Type: checker.DependencyUseTypeNpmCommand, }, ) @@ -854,7 +845,7 @@ func collectUnpinnedPakageManagerDownload(startLine, endLine uint, node syntax.N } // Choco install. - if isChocoUnpinnedDownload(c) { + if isChocoDownload(c) { r.Dependencies = append(r.Dependencies, checker.Dependency{ Location: &checker.File{ @@ -864,7 +855,8 @@ func collectUnpinnedPakageManagerDownload(startLine, endLine uint, node syntax.N EndOffset: endLine, Snippet: cmd, }, - Type: checker.DependencyUseTypeChocoCommand, + Pinned: asBoolPointer(!isChocoUnpinnedDownload(c)), + Type: checker.DependencyUseTypeChocoCommand, }, ) @@ -872,7 +864,7 @@ func collectUnpinnedPakageManagerDownload(startLine, endLine uint, node syntax.N } // Nuget install. - if isNugetUnpinnedDownload(c) { + if isNugetDownload(c) { r.Dependencies = append(r.Dependencies, checker.Dependency{ Location: &checker.File{ @@ -882,7 +874,8 @@ func collectUnpinnedPakageManagerDownload(startLine, endLine uint, node syntax.N EndOffset: endLine, Snippet: cmd, }, - Type: checker.DependencyUseTypeNugetCommand, + Pinned: asBoolPointer(!isNugetUnpinnedDownload(c)), + Type: checker.DependencyUseTypeNugetCommand, }, ) @@ -977,7 +970,8 @@ func collectFetchProcSubsExecute(startLine, endLine uint, node syntax.Node, cmd, EndOffset: endLine, Snippet: cmd, }, - Type: checker.DependencyUseTypeDownloadThenRun, + Pinned: asBoolPointer(false), + Type: checker.DependencyUseTypeDownloadThenRun, }, ) } diff --git a/checks/raw/shell_download_validate_test.go b/checks/raw/shell_download_validate_test.go index 4624353e29c..efc22927367 100644 --- a/checks/raw/shell_download_validate_test.go +++ b/checks/raw/shell_download_validate_test.go @@ -242,3 +242,69 @@ func Test_isGoUnpinnedDownload(t *testing.T) { }) } } + +func Test_isNpmDownload(t *testing.T) { + type args struct { + cmd []string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "npm install", + args: args{ + cmd: []string{"npm", "install"}, + }, + want: true, + }, + { + name: "npm ci", + args: args{ + cmd: []string{"npm", "ci"}, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isNpmDownload(tt.args.cmd); got != tt.want { + t.Errorf("isNpmDownload() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_isNpmUnpinnedDownload(t *testing.T) { + type args struct { + cmd []string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "npm install", + args: args{ + cmd: []string{"npm", "install"}, + }, + want: true, + }, + { + name: "npm ci", + args: args{ + cmd: []string{"npm", "ci"}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isNpmUnpinnedDownload(tt.args.cmd); got != tt.want { + t.Errorf("isNpmUnpinnedDownload() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/checks/testdata/.github/workflows/github-workflow-permissions-contents-writes-release-semantic-release-pnpm.yaml b/checks/testdata/.github/workflows/github-workflow-permissions-contents-writes-release-semantic-release-pnpm.yaml new file mode 100644 index 00000000000..b2fb0a572c6 --- /dev/null +++ b/checks/testdata/.github/workflows/github-workflow-permissions-contents-writes-release-semantic-release-pnpm.yaml @@ -0,0 +1,29 @@ +# Copyright 2022 Security Scorecard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +name: semantic-release release workflow +on: [push] +permissions: + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + - name: semantic-release + run: yarn -s semantic-release + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/checks/testdata/.github/workflows/github-workflow-permissions-contents-writes-release-semantic-release-yarn.yaml b/checks/testdata/.github/workflows/github-workflow-permissions-contents-writes-release-semantic-release-yarn.yaml new file mode 100644 index 00000000000..b3f5bdfdd12 --- /dev/null +++ b/checks/testdata/.github/workflows/github-workflow-permissions-contents-writes-release-semantic-release-yarn.yaml @@ -0,0 +1,29 @@ +# Copyright 2022 Security Scorecard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +name: semantic-release release workflow +on: [push] +permissions: + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + - name: semantic-release + run: pnpm dlx semantic-release + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/clients/githubrepo/roundtripper/tokens/server/Dockerfile b/clients/githubrepo/roundtripper/tokens/server/Dockerfile index c8c9ca36a34..014a9afdc78 100644 --- a/clients/githubrepo/roundtripper/tokens/server/Dockerfile +++ b/clients/githubrepo/roundtripper/tokens/server/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM golang:1.21@sha256:ec457a2fcd235259273428a24e09900c496d0c52207266f96a330062a01e3622 AS base +FROM golang:1.21@sha256:e9ebfe932adeff65af5338236f0b0604c86b143c1bff3e1d0551d8f6196ab5c5 AS base WORKDIR /src ENV CGO_ENABLED=0 COPY go.* ./ @@ -24,6 +24,6 @@ ARG TARGETOS ARG TARGETARCH RUN CGO_ENABLED=0 make build-github-server -FROM gcr.io/distroless/base:nonroot@sha256:27647a684d554b6640e32c549dacb3c898c2632fedd0e822b6ffdc24c1c18150 +FROM gcr.io/distroless/base:nonroot@sha256:29da700a46816467c7cb91058f53eac4170a4a25ac8551d316d9fd38e2c58bdf COPY --from=authserver /src/clients/githubrepo/roundtripper/tokens/server/github-auth-server clients/githubrepo/roundtripper/tokens/server/github-auth-server ENTRYPOINT ["clients/githubrepo/roundtripper/tokens/server/github-auth-server"] diff --git a/clients/gitlabrepo/licenses.go b/clients/gitlabrepo/licenses.go index 7b76963dde6..7208ff815c7 100644 --- a/clients/gitlabrepo/licenses.go +++ b/clients/gitlabrepo/licenses.go @@ -46,6 +46,11 @@ func (handler *licensesHandler) setup() error { handler.once.Do(func() { l := handler.glProject.License + // No registered license on GitLab repo, use file-based license detection instead + if l == nil { + return + } + ptn, err := regexp.Compile(fmt.Sprintf("%s/-/blob/(?:\\w+)/(.*)", handler.repourl.URI())) if err != nil { handler.errSetup = fmt.Errorf("couldn't parse license url: %w", err) diff --git a/cmd/internal/scdiff/app/compare.go b/cmd/internal/scdiff/app/compare.go index 7947c0f721c..8ff1117ffcc 100644 --- a/cmd/internal/scdiff/app/compare.go +++ b/cmd/internal/scdiff/app/compare.go @@ -36,18 +36,15 @@ func init() { } var ( - errMissingInputFiles = errors.New("must provide at least two files from scdiff generate") - errResultsDiffer = errors.New("results differ") - errNumResults = errors.New("number of results being compared differ") + errResultsDiffer = errors.New("results differ") + errNumResults = errors.New("number of results being compared differ") compareCmd = &cobra.Command{ Use: "compare [flags] FILE1 FILE2", Short: "Compare Scorecard results", Long: `Compare Scorecard results`, + Args: cobra.MinimumNArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - if len(args) < 2 { - return errMissingInputFiles - } f1, err := os.Open(args[0]) if err != nil { return fmt.Errorf("opening %q: %w", args[0], err) @@ -68,7 +65,9 @@ var ( func compareReaders(x, y io.Reader, output io.Writer) error { // results are currently newline delimited xs := bufio.NewScanner(x) + xs.Buffer(nil, maxResultSize) ys := bufio.NewScanner(y) + ys.Buffer(nil, maxResultSize) for { if shouldContinue, err := advanceScanners(xs, ys); err != nil { return err @@ -90,11 +89,11 @@ func compareReaders(x, y io.Reader, output io.Writer) error { } func loadResults(x, y *bufio.Scanner) (pkg.ScorecardResult, pkg.ScorecardResult, error) { - xResult, err := pkg.ExperimentalFromJSON2(strings.NewReader(x.Text())) + xResult, _, err := pkg.ExperimentalFromJSON2(strings.NewReader(x.Text())) if err != nil { return pkg.ScorecardResult{}, pkg.ScorecardResult{}, fmt.Errorf("parsing first result: %w", err) } - yResult, err := pkg.ExperimentalFromJSON2(strings.NewReader(y.Text())) + yResult, _, err := pkg.ExperimentalFromJSON2(strings.NewReader(y.Text())) if err != nil { return pkg.ScorecardResult{}, pkg.ScorecardResult{}, fmt.Errorf("parsing second result: %w", err) } diff --git a/cmd/internal/scdiff/app/generate.go b/cmd/internal/scdiff/app/generate.go index 3cf29acd8ac..d3a6399a3fa 100644 --- a/cmd/internal/scdiff/app/generate.go +++ b/cmd/internal/scdiff/app/generate.go @@ -19,6 +19,7 @@ import ( "fmt" "io" "os" + "strings" "github.com/spf13/cobra" @@ -32,11 +33,13 @@ func init() { rootCmd.AddCommand(generateCmd) generateCmd.PersistentFlags().StringVarP(&repoFile, "repos", "r", "", "path to newline-delimited repo file") generateCmd.PersistentFlags().StringVarP(&outputFile, "output", "o", "", "write to file instead of stdout") + generateCmd.PersistentFlags().StringVar(&checksArg, "checks", "", "Comma separated list of checks to run") } var ( repoFile string outputFile string + checksArg string generateCmd = &cobra.Command{ Use: "generate [flags] repofile", @@ -57,7 +60,8 @@ var ( defer outputF.Close() output = outputF } - r := runner.New() + checks := strings.Split(checksArg, ",") + r := runner.New(checks) return generate(&r, input, output) }, } diff --git a/cmd/internal/scdiff/app/runner/runner.go b/cmd/internal/scdiff/app/runner/runner.go index 4a65e1f7a51..535bc7a84f2 100644 --- a/cmd/internal/scdiff/app/runner/runner.go +++ b/cmd/internal/scdiff/app/runner/runner.go @@ -16,6 +16,7 @@ package runner import ( "context" + "strings" "github.com/ossf/scorecard/v4/checker" "github.com/ossf/scorecard/v4/checks" @@ -42,7 +43,8 @@ type Runner struct { vuln clients.VulnerabilitiesClient } -func New() Runner { +// Creates a Runner which will run the listed checks. If no checks are provided, all will run. +func New(enabledChecks []string) Runner { ctx := context.Background() logger := log.NewLogger(log.DefaultLevel) return Runner{ @@ -52,7 +54,7 @@ func New() Runner { ossFuzz: ossfuzz.CreateOSSFuzzClient(ossfuzz.StatusURL), cii: clients.DefaultCIIBestPracticesClient(), vuln: clients.DefaultVulnerabilitiesClient(), - enabledChecks: checks.GetAll(), + enabledChecks: parseChecks(enabledChecks), } } @@ -73,3 +75,20 @@ func (r *Runner) log(msg string) { r.logger.Info(msg) } } + +func parseChecks(c []string) checker.CheckNameToFnMap { + all := checks.GetAll() + if len(c) == 0 { + return all + } + + ret := checker.CheckNameToFnMap{} + for _, requested := range c { + for key, fn := range all { + if strings.EqualFold(key, requested) { + ret[key] = fn + } + } + } + return ret +} diff --git a/cmd/internal/scdiff/app/runner/runner_test.go b/cmd/internal/scdiff/app/runner/runner_test.go index 8d44ce10345..1b4c05e3c4a 100644 --- a/cmd/internal/scdiff/app/runner/runner_test.go +++ b/cmd/internal/scdiff/app/runner/runner_test.go @@ -25,10 +25,15 @@ import ( ) func TestNew(t *testing.T) { - r := New() + r := New(nil) if len(r.enabledChecks) == 0 { t.Errorf("runner has no checks to run: %v", r.enabledChecks) } + requestedChecks := []string{"Code-Review"} + r = New(requestedChecks) + if len(r.enabledChecks) != len(requestedChecks) { + t.Errorf("requested %d checks but only got: %v", len(requestedChecks), r.enabledChecks) + } } func TestRunner_Run(t *testing.T) { diff --git a/cmd/internal/scdiff/app/stats.go b/cmd/internal/scdiff/app/stats.go new file mode 100644 index 00000000000..d1a09eff6b8 --- /dev/null +++ b/cmd/internal/scdiff/app/stats.go @@ -0,0 +1,127 @@ +// Copyright 2023 OpenSSF Scorecard Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package app + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + + "github.com/spf13/cobra" + "golang.org/x/exp/slices" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/pkg" +) + +//nolint:gochecknoinits // common for cobra apps +func init() { + rootCmd.AddCommand(statsCmd) + statsCmd.PersistentFlags().StringVarP(&statsCheck, "check", "c", "", "Analyze breakdown of a single check") +} + +// 1 MiB size limit for individual results. This currently works, +// but bufio.Scanner always has a limit, may need to change approach in the future. +const maxResultSize = 1024 * 1024 + +var ( + statsCheck string + statsCmd = &cobra.Command{ + Use: "stats [flags] FILE", + Short: "Summarize stats for a golden file", + Long: `Summarize stats for a golden file`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + f1, err := os.Open(args[0]) + if err != nil { + return fmt.Errorf("opening %q: %w", args[0], err) + } + defer f1.Close() + return calcStats(f1, os.Stdout) + }, + } + + errCheckNotPresent = errors.New("requested check not present") + errInvalidScore = errors.New("invalid score") +) + +// countScores quantizes the scores into 12 buckets, from [-1, 10] +// If a check is provided, that check score is used, otherwise the aggregate score is used. +func countScores(input io.Reader, check string) ([12]int, error) { + var counts [12]int // [-1, 10] inclusive + var score int + scanner := bufio.NewScanner(input) + scanner.Buffer(nil, maxResultSize) + for scanner.Scan() { + result, aggregateScore, err := pkg.ExperimentalFromJSON2(strings.NewReader(scanner.Text())) + if err != nil { + return [12]int{}, fmt.Errorf("parsing result: %w", err) + } + if check == "" { + score = int(aggregateScore) + } else { + i := slices.IndexFunc(result.Checks, func(c checker.CheckResult) bool { + return strings.EqualFold(c.Name, check) + }) + if i == -1 { + return [12]int{}, errCheckNotPresent + } + score = result.Checks[i].Score + } + if score < -1 || score > 10 { + return [12]int{}, errInvalidScore + } + bucket := score + 1 // score of -1 is index 0, score of 0 is index 1, etc. + counts[bucket]++ + } + if err := scanner.Err(); err != nil { + return [12]int{}, fmt.Errorf("parsing golden file: %w", err) + } + return counts, nil +} + +func calcStats(input io.Reader, output io.Writer) error { + counts, err := countScores(input, statsCheck) + if err != nil { + return err + } + name := statsCheck + if name == "" { + name = "Aggregate" + } + summary(name, &counts, output) + return nil +} + +func summary(name string, counts *[12]int, output io.Writer) { + const ( + minWidth = 0 + tabWidth = 4 + padding = 1 + padchar = ' ' + flags = tabwriter.AlignRight + ) + w := tabwriter.NewWriter(output, minWidth, tabWidth, padding, padchar, flags) + fmt.Fprintf(w, "%s Score\tCount\t\n", name) + for i, c := range counts { + scoreBucket := i - 1 + fmt.Fprintf(w, "%d\t%d\t\n", scoreBucket, c) + } + w.Flush() +} diff --git a/cmd/internal/scdiff/app/stats_test.go b/cmd/internal/scdiff/app/stats_test.go new file mode 100644 index 00000000000..7896a80d38a --- /dev/null +++ b/cmd/internal/scdiff/app/stats_test.go @@ -0,0 +1,109 @@ +// Copyright 2023 OpenSSF Scorecard Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package app + +import ( + "bytes" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_countScores(t *testing.T) { + t.Parallel() + + common := `{"date":"0001-01-01T00:00:00Z","repo":{"name":"repo1"},"score":0,"checks":[{"score":10,"name":"Foo"}]} +{"date":"0001-01-01T00:00:00Z","repo":{"name":"repo2"},"score":-1,"checks":[{"score":9,"name":"Foo"}]} +` + tests := []struct { + name string + check string + results string + want [12]int + wantErr bool + }{ + { + name: "aggregate score used when no check specified", + results: common, + want: [12]int{1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + }, + { + name: "check score used when check specified", + check: "Foo", + results: common, + want: [12]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1}, + }, + { + name: "check name case insensitive", + check: "fOo", + results: common, + want: [12]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1}, + }, + { + name: "non existent check", + check: "not present", + results: common, + wantErr: true, + }, + { + name: "score outside of [-1, 10] rejected", + results: `{"date":"0001-01-01T00:00:00Z","repo":{"name":"repo1"},"score":12}`, + wantErr: true, + }, + { + name: "result fails to parse", + results: `]}[{}`, + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + input := strings.NewReader(tt.results) + got, err := countScores(input, tt.check) + if (err != nil) != tt.wantErr { + t.Fatalf("unexpected error: got %v, wantedErr: %t", err, tt.wantErr) + } + if got != tt.want { + t.Errorf("counts differ: %v", cmp.Diff(got, tt.want)) + } + }) + } +} + +func removeRepeatedSpaces(t *testing.T, s string) string { + t.Helper() + return strings.Join(strings.Fields(s), " ") +} + +func Test_calcStats(t *testing.T) { + t.Parallel() + input := strings.NewReader(`{"date":"0001-01-01T00:00:00Z","repo":{"name":"repo1"},"score":10}`) + var output bytes.Buffer + if err := calcStats(input, &output); err != nil { + t.Fatalf("unexepected error: %v", err) + } + got := output.String() + // this is a bit of a simplification, but keeps the test simple + // without needing to update every minor formatting change. + got = removeRepeatedSpaces(t, got) + want := "10 1" // score 10 should have count 1 + if !strings.Contains(got, want) { + t.Errorf("didn't contain expected count") + } +} diff --git a/cron/internal/bq/Dockerfile b/cron/internal/bq/Dockerfile index 1baa497bf68..e8b9e31ed37 100644 --- a/cron/internal/bq/Dockerfile +++ b/cron/internal/bq/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM golang:1.21@sha256:ec457a2fcd235259273428a24e09900c496d0c52207266f96a330062a01e3622 AS base +FROM golang:1.21@sha256:e9ebfe932adeff65af5338236f0b0604c86b143c1bff3e1d0551d8f6196ab5c5 AS base WORKDIR /src ENV CGO_ENABLED=0 COPY go.* ./ @@ -24,6 +24,6 @@ ARG TARGETOS ARG TARGETARCH RUN CGO_ENABLED=0 make build-bq-transfer -FROM gcr.io/distroless/base:nonroot@sha256:27647a684d554b6640e32c549dacb3c898c2632fedd0e822b6ffdc24c1c18150 +FROM gcr.io/distroless/base:nonroot@sha256:29da700a46816467c7cb91058f53eac4170a4a25ac8551d316d9fd38e2c58bdf COPY --from=transfer /src/cron/internal/bq/data-transfer cron/internal/bq/data-transfer ENTRYPOINT ["cron/internal/bq/data-transfer"] diff --git a/cron/internal/cii/Dockerfile b/cron/internal/cii/Dockerfile index 61c7fc59dd9..1f619ddade3 100644 --- a/cron/internal/cii/Dockerfile +++ b/cron/internal/cii/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM golang:1.21@sha256:ec457a2fcd235259273428a24e09900c496d0c52207266f96a330062a01e3622 AS base +FROM golang:1.21@sha256:e9ebfe932adeff65af5338236f0b0604c86b143c1bff3e1d0551d8f6196ab5c5 AS base WORKDIR /src ENV CGO_ENABLED=0 COPY go.* ./ @@ -24,6 +24,6 @@ ARG TARGETOS ARG TARGETARCH RUN CGO_ENABLED=0 make build-cii-worker -FROM gcr.io/distroless/base:nonroot@sha256:27647a684d554b6640e32c549dacb3c898c2632fedd0e822b6ffdc24c1c18150 +FROM gcr.io/distroless/base:nonroot@sha256:29da700a46816467c7cb91058f53eac4170a4a25ac8551d316d9fd38e2c58bdf COPY --from=cii /src/cron/internal/cii/cii-worker cron/internal/cii/cii-worker ENTRYPOINT ["cron/internal/cii/cii-worker"] diff --git a/cron/internal/controller/Dockerfile b/cron/internal/controller/Dockerfile index 149e98e7831..7c4bc131174 100644 --- a/cron/internal/controller/Dockerfile +++ b/cron/internal/controller/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM golang:1.21@sha256:ec457a2fcd235259273428a24e09900c496d0c52207266f96a330062a01e3622 AS base +FROM golang:1.21@sha256:e9ebfe932adeff65af5338236f0b0604c86b143c1bff3e1d0551d8f6196ab5c5 AS base WORKDIR /src ENV CGO_ENABLED=0 COPY go.* ./ @@ -30,7 +30,7 @@ ARG TARGETOS ARG TARGETARCH RUN CGO_ENABLED=0 make build-controller -FROM gcr.io/distroless/base:nonroot@sha256:27647a684d554b6640e32c549dacb3c898c2632fedd0e822b6ffdc24c1c18150 +FROM gcr.io/distroless/base:nonroot@sha256:29da700a46816467c7cb91058f53eac4170a4a25ac8551d316d9fd38e2c58bdf COPY ./cron/internal/data/projects*csv cron/internal/data/ COPY ./cron/internal/data/gitlab-projects-releasetest.csv cron/internal/data/ COPY ./cron/internal/data/gitlab-projects.csv cron/internal/data/ diff --git a/cron/internal/webhook/Dockerfile b/cron/internal/webhook/Dockerfile index 13660cde02e..0e7865dd052 100644 --- a/cron/internal/webhook/Dockerfile +++ b/cron/internal/webhook/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM golang:1.21@sha256:ec457a2fcd235259273428a24e09900c496d0c52207266f96a330062a01e3622 AS base +FROM golang:1.21@sha256:e9ebfe932adeff65af5338236f0b0604c86b143c1bff3e1d0551d8f6196ab5c5 AS base WORKDIR /src ENV CGO_ENABLED=0 COPY go.* ./ @@ -24,6 +24,6 @@ ARG TARGETOS ARG TARGETARCH RUN CGO_ENABLED=0 make build-webhook -FROM gcr.io/distroless/base:nonroot@sha256:27647a684d554b6640e32c549dacb3c898c2632fedd0e822b6ffdc24c1c18150 +FROM gcr.io/distroless/base:nonroot@sha256:29da700a46816467c7cb91058f53eac4170a4a25ac8551d316d9fd38e2c58bdf COPY --from=webhook /src/cron/internal/webhook/webhook cron/internal/webhook/webhook ENTRYPOINT ["cron/internal/webhook/webhook"] diff --git a/cron/internal/worker/Dockerfile b/cron/internal/worker/Dockerfile index d6c92fcc189..00232784b86 100644 --- a/cron/internal/worker/Dockerfile +++ b/cron/internal/worker/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM golang:1.21@sha256:ec457a2fcd235259273428a24e09900c496d0c52207266f96a330062a01e3622 AS base +FROM golang:1.21@sha256:e9ebfe932adeff65af5338236f0b0604c86b143c1bff3e1d0551d8f6196ab5c5 AS base WORKDIR /src ENV CGO_ENABLED=0 COPY go.* ./ @@ -24,6 +24,6 @@ ARG TARGETOS ARG TARGETARCH RUN CGO_ENABLED=0 make build-worker -FROM gcr.io/distroless/base:nonroot@sha256:27647a684d554b6640e32c549dacb3c898c2632fedd0e822b6ffdc24c1c18150 +FROM gcr.io/distroless/base:nonroot@sha256:29da700a46816467c7cb91058f53eac4170a4a25ac8551d316d9fd38e2c58bdf COPY --from=worker /src/cron/internal/worker/worker cron/internal/worker/worker ENTRYPOINT ["cron/internal/worker/worker"] diff --git a/docs/checks.md b/docs/checks.md index a7a130e03a6..c360fabff87 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -83,7 +83,7 @@ Different types of branch protection protect against different risks: - requiring two or more reviewers protects even more from the insider risk whereby a compromised contributor can be used by an attacker to LGTM - the attacker PR and inject a malicious code as if it was legitm. + the attacker PR and inject a malicious code as if it was legit. - Prevent force push: prevents use of the `--force` command on public branches, which overwrites code irrevocably. This protection prevents the @@ -109,7 +109,6 @@ Note: If Scorecard is run without an administrative access token, the requiremen Tier 1 Requirements (3/10 points): - Prevent force push - Prevent branch deletion - - For administrators: Include administrator for review Tier 2 Requirements (6/10 points): - Require at least 1 reviewer for approval before merging @@ -125,6 +124,7 @@ Tier 4 Requirements (9/10 points): Tier 5 Requirements (10/10 points): - For administrators: Dismiss stale reviews and approvals when new commits are pushed + - For administrators: Include administrator for review GitLab Integration Status: - GitLab associates releases with commits and not with the branch. Releases are ignored in this portion of the scoring. @@ -329,7 +329,7 @@ low score is therefore not a definitive indication that the project is at risk. **Remediation steps** - Signup for automatic dependency updates with one of the previously listed dependency update tools and place the config file in the locations that are recommended by these tools. Due to https://github.com/dependabot/dependabot-core/issues/2804 Dependabot can be enabled for forks where security updates have ever been turned on so projects maintaining stable forks should evaluate whether this behavior is satisfactory before turning it on. -- Unlike Dependabot, Renovate bot has support to migrate dockerfiles' dependencies from version pinning to hash pinning via the [pinDigests setting](https://docs.renovatebot.com/configuration-options/#pindigests) without aditional manual effort. +- Unlike Dependabot, Renovate bot has support to migrate dockerfiles' dependencies from version pinning to hash pinning via the [pinDigests setting](https://docs.renovatebot.com/configuration-options/#pindigests) without additional manual effort. ## Fuzzing @@ -600,6 +600,8 @@ This check looks for the following filenames in the project's last five If a signature is found in the assets for each release, a score of 8 is given. If a [SLSA provenance file](https://slsa.dev/spec/v0.1/index) is found in the assets for each release (*.intoto.jsonl), the maximum score of 10 is given. +This check looks for the 30 most recent releases associated with an artifact. It ignores the source code-only releases that are created automatically by GitHub. + Note: The check does not verify the signatures. diff --git a/docs/checks/internal/checks.yaml b/docs/checks/internal/checks.yaml index 352cc1ad107..295cf683d05 100644 --- a/docs/checks/internal/checks.yaml +++ b/docs/checks/internal/checks.yaml @@ -87,7 +87,7 @@ checks: - >- Unlike Dependabot, Renovate bot has support to migrate dockerfiles' dependencies from version pinning to hash pinning via the [pinDigests setting](https://docs.renovatebot.com/configuration-options/#pindigests) without - aditional manual effort. + additional manual effort. Binary-Artifacts: risk: High tags: supply-chain, security, dependencies @@ -172,7 +172,7 @@ checks: - requiring two or more reviewers protects even more from the insider risk whereby a compromised contributor can be used by an attacker to LGTM - the attacker PR and inject a malicious code as if it was legitm. + the attacker PR and inject a malicious code as if it was legit. - Prevent force push: prevents use of the `--force` command on public branches, which overwrites code irrevocably. This protection prevents the @@ -198,7 +198,6 @@ checks: Tier 1 Requirements (3/10 points): - Prevent force push - Prevent branch deletion - - For administrators: Include administrator for review Tier 2 Requirements (6/10 points): - Require at least 1 reviewer for approval before merging @@ -214,6 +213,7 @@ checks: Tier 5 Requirements (10/10 points): - For administrators: Dismiss stale reviews and approvals when new commits are pushed + - For administrators: Include administrator for review GitLab Integration Status: - GitLab associates releases with commits and not with the branch. Releases are ignored in this portion of the scoring. @@ -632,6 +632,8 @@ checks: If a signature is found in the assets for each release, a score of 8 is given. If a [SLSA provenance file](https://slsa.dev/spec/v0.1/index) is found in the assets for each release (*.intoto.jsonl), the maximum score of 10 is given. + This check looks for the 30 most recent releases associated with an artifact. It ignores the source code-only releases that are created automatically by GitHub. + Note: The check does not verify the signatures. remediation: - >- diff --git a/docs/faq.md b/docs/faq.md index 2fd5f6ffb8a..18deacf659c 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -25,7 +25,11 @@ This page answers frequently asked questions about Scorecard, including its purp Yes. -Over a million projects are automatically tracked by the Scorecard project. These projects' scores can be seen at https://api.securityscorecards.dev/projects/github.com//. +Over a million projects are automatically tracked by the Scorecard project. Use the webviewer to see these scores, replacing the placeholder text with the platform, user/org, and repository name: https://securityscorecards.dev/viewer/?uri=.com//. + +For example: + - [https://securityscorecards.dev/viewer/?uri=github.com/ossf/scorecard](https://securityscorecards.dev/viewer/?uri=github.com/ossf/scorecard) + - [https://securityscorecards.dev/viewer/?uri=gitlab.com/fdroid/fdroidclient](https://securityscorecards.dev/viewer/?uri=gitlab.com/fdroid/fdroidclient) You can also use the CLI to generate scores for any public repository by following these steps: @@ -39,7 +43,7 @@ Most code scanning tools are focused on detecting specific vulnerabilities alrea ### Wasn't this project called "Scorecards" (plural)? -Yes, kind of. The project was initially called "Security Scorecards" but that form wasn't used consistently. In particular, the repo was named "scorecard" and so was the program. Over time people started referring to either form (singular and plural) and the inconsitency became prevalent. To end this situation the decision was made to consolidate over the use of the singular form in keeping with the repo and program name, drop the "Security" part and use "OpenSSF" instead to ensure uniqueness. One should therefore refer to this project as "OpenSSF Scorecard" or "Scorecard" for short. +Yes, kind of. The project was initially called "Security Scorecards" but that form wasn't used consistently. In particular, the repo was named "scorecard" and so was the program. Over time people started referring to either form (singular and plural) and the inconsistency became prevalent. To end this situation the decision was made to consolidate over the use of the singular form in keeping with the repo and program name, drop the "Security" part and use "OpenSSF" instead to ensure uniqueness. One should therefore refer to this project as "OpenSSF Scorecard" or "Scorecard" for short. ## Check-specific Questions @@ -51,7 +55,7 @@ While it isn't currently possible to allowlist such binaries, the Scorecard team ### Code-Review: Can it ignore bot commits? -This is quite a complex question. Right now, there is no way to do that. Here are some pros and cons on allowing users to set up an ignore-list for bots. +This is quite a complex question. Right now, there is no way to do that. Here are some pros and cons of allowing users to set up an ignore-list for bots. - Pros: Some bots run very frequently; for some projects, reviewing every change is therefore not feasible or reasonable. - Cons: Bots can be compromised (their credentials can be compromised, for example). Or if commits are not signed, an attacker could easily send a commit spoofing the bot. This means that a bot having unsupervised write access to the repository could be a security risk. diff --git a/e2e/pinned_dependencies_test.go b/e2e/pinned_dependencies_test.go index c49357d337e..924a43a1ce1 100644 --- a/e2e/pinned_dependencies_test.go +++ b/e2e/pinned_dependencies_test.go @@ -49,9 +49,9 @@ var _ = Describe("E2E TEST:"+checks.CheckPinnedDependencies, func() { } expected := scut.TestReturn{ Error: nil, - Score: 4, + Score: 2, NumberOfWarn: 139, - NumberOfInfo: 3, + NumberOfInfo: 5, NumberOfDebug: 0, } result := checks.PinningDependencies(&req) @@ -74,9 +74,9 @@ var _ = Describe("E2E TEST:"+checks.CheckPinnedDependencies, func() { } expected := scut.TestReturn{ Error: nil, - Score: 4, + Score: 2, NumberOfWarn: 139, - NumberOfInfo: 3, + NumberOfInfo: 5, NumberOfDebug: 0, } result := checks.PinningDependencies(&req) @@ -110,9 +110,9 @@ var _ = Describe("E2E TEST:"+checks.CheckPinnedDependencies, func() { } expected := scut.TestReturn{ Error: nil, - Score: 4, + Score: 2, NumberOfWarn: 139, - NumberOfInfo: 3, + NumberOfInfo: 5, NumberOfDebug: 0, } result := checks.PinningDependencies(&req) diff --git a/finding/finding.go b/finding/finding.go index 4d9b2ca8d87..b9b8507ee95 100644 --- a/finding/finding.go +++ b/finding/finding.go @@ -84,6 +84,12 @@ const ( _ // OutcomeNotSupported indicates a non-supported outcome. OutcomeNotSupported + _ + _ + _ + // OutcomeNotApplicable indicates if a finding should not + // be considered in evaluation. + OutcomeNotApplicable ) // Finding represents a finding. diff --git a/go.mod b/go.mod index 5f6ec5f5506..2e1ae36a0a5 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,7 @@ module github.com/ossf/scorecard/v4 go 1.19 require ( - github.com/rhysd/actionlint v1.6.15 - gotest.tools v2.2.0+incompatible -) - -require ( - cloud.google.com/go/bigquery v1.55.0 + cloud.google.com/go/bigquery v1.56.0 cloud.google.com/go/monitoring v1.15.1 // indirect cloud.google.com/go/pubsub v1.33.0 cloud.google.com/go/trace v1.10.1 // indirect @@ -25,7 +20,8 @@ require ( github.com/jszwec/csvutil v1.8.0 github.com/moby/buildkit v0.12.2 github.com/olekukonko/tablewriter v0.0.5 - github.com/onsi/gomega v1.27.10 + github.com/onsi/gomega v1.28.0 + github.com/rhysd/actionlint v1.6.26 github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a github.com/sirupsen/logrus v1.9.3 @@ -35,7 +31,7 @@ require ( gocloud.dev v0.34.0 golang.org/x/text v0.13.0 golang.org/x/tools v0.13.0 // indirect - google.golang.org/genproto v0.0.0-20230731193218-e0aa005b6bdf // indirect + google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect google.golang.org/protobuf v1.31.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -47,10 +43,10 @@ require ( github.com/caarlos0/env/v6 v6.10.0 github.com/gobwas/glob v0.2.3 github.com/google/go-github/v53 v53.2.0 - github.com/google/osv-scanner v1.4.0 + github.com/google/osv-scanner v1.4.1 github.com/mcuadros/go-jsonschema-generator v0.0.0-20200330054847-ba7a369d4303 - github.com/onsi/ginkgo/v2 v2.12.0 - github.com/otiai10/copy v1.12.0 + github.com/onsi/ginkgo/v2 v2.12.1 + github.com/otiai10/copy v1.14.0 sigs.k8s.io/release-utils v0.6.0 ) @@ -61,7 +57,7 @@ require ( dario.cat/mergo v1.0.0 // indirect github.com/BurntSushi/toml v1.3.2 // indirect github.com/CycloneDX/cyclonedx-go v0.7.2 // indirect - github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect + github.com/anchore/go-struct-converter v0.0.0-20230627203149-c72ef8859ca9 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/apache/arrow/go/v12 v12.0.0 // indirect github.com/apache/thrift v0.16.0 // indirect @@ -74,7 +70,7 @@ require ( github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/goark/errs v1.1.0 // indirect + github.com/goark/errs v1.3.2 // indirect github.com/goark/go-cvss v1.6.6 // indirect github.com/goccy/go-json v0.9.11 // indirect github.com/golang/glog v1.1.0 // indirect @@ -84,7 +80,7 @@ require ( github.com/google/go-github/v55 v55.0.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect - github.com/google/s2a-go v0.1.4 // indirect + github.com/google/s2a-go v0.1.7 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.4 // indirect github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab // indirect @@ -99,21 +95,22 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/owenrumney/go-sarif/v2 v2.2.0 // indirect + github.com/owenrumney/go-sarif/v2 v2.2.2 // indirect github.com/package-url/packageurl-go v0.1.1 // indirect github.com/pierrec/lz4/v4 v4.1.15 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/prometheus/prometheus v0.46.0 // indirect - github.com/skeema/knownhosts v1.2.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/skeema/knownhosts v1.2.1 // indirect github.com/spdx/gordf v0.0.0-20221230105357-b735bd5aac89 // indirect github.com/spdx/tools-golang v0.5.3 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect golang.org/x/mod v0.12.0 // indirect - golang.org/x/term v0.12.0 // indirect + golang.org/x/term v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/vuln v1.0.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230731193218-e0aa005b6bdf // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230731193218-e0aa005b6bdf // indirect + golang.org/x/vuln v1.0.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/api v0.27.3 // indirect k8s.io/apimachinery v0.27.3 // indirect @@ -132,7 +129,7 @@ require ( cloud.google.com/go/iam v1.1.1 // indirect cloud.google.com/go/storage v1.31.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect github.com/acomagu/bufpipe v1.0.4 // indirect github.com/aws/aws-sdk-go v1.44.314 // indirect github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect @@ -163,28 +160,27 @@ require ( github.com/klauspost/compress v1.16.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc3 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/rivo/uniseg v0.2.0 // indirect - github.com/robfig/cron v1.2.0 // indirect - github.com/sergi/go-diff v1.1.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/sergi/go-diff v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/vbatts/tar-split v0.11.3 // indirect - github.com/xanzy/go-gitlab v0.91.1 + github.com/xanzy/go-gitlab v0.93.1 github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - golang.org/x/crypto v0.13.0 // indirect + golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 - golang.org/x/net v0.15.0 // indirect - golang.org/x/oauth2 v0.12.0 + golang.org/x/net v0.16.0 // indirect + golang.org/x/oauth2 v0.13.0 golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/sys v0.13.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/api v0.134.0 // indirect + google.golang.org/api v0.139.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/grpc v1.57.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/go.sum b/go.sum index fbb2d02c134..e41df94f5e9 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5x cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.55.0 h1:hs44Xxov3XLWQiCx2J8lK5U/ihLqnpm4RVVl5fdtLLI= -cloud.google.com/go/bigquery v1.55.0/go.mod h1:9Y5I3PN9kQWuid6183JFhOGOW3GcirA5LpsKCUn+2ec= +cloud.google.com/go/bigquery v1.56.0 h1:LHIc9E7Kw+ftFpQFKzZYBB88IAFz7qONawXXx0F3QBo= +cloud.google.com/go/bigquery v1.56.0/go.mod h1:KDcsploXTEY7XT3fDQzMUZlpQLHzE4itubHrnmhUrZA= cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= @@ -94,8 +94,8 @@ github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5 github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE= +github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= @@ -104,8 +104,9 @@ github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= +github.com/anchore/go-struct-converter v0.0.0-20230627203149-c72ef8859ca9 h1:6COpXWpHbhWM1wgcQN95TdsmrLTba8KQfPgImBXzkjA= +github.com/anchore/go-struct-converter v0.0.0-20230627203149-c72ef8859ca9/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -162,7 +163,6 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -172,11 +172,7 @@ github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEM github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= @@ -243,7 +239,6 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v0.0.0-20200808040245-162e5629780b/go.mod h1:NAJj0yf/KaRKURN6nyi7A9IZydMivZEm9oQLWNjfKDc= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -300,8 +295,8 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/goark/errs v1.1.0 h1:FKnyw4LVyRADIjM8Nj0Up6r0/y5cfADvZAd1E+tthXE= -github.com/goark/errs v1.1.0/go.mod h1:TtaPEoadm2mzqzfXdkkfpN2xuniCFm2q4JH+c1qzaqw= +github.com/goark/errs v1.3.2 h1:ifccNe1aK7Xezt4XVYwHUqalmnfhuphnEvh3FshCReQ= +github.com/goark/errs v1.3.2/go.mod h1:ZsQucxaDFVfSB8I99j4bxkDRfNOrlKINwg72QMuRWKw= github.com/goark/go-cvss v1.6.6 h1:WJFuIWqmAw1Ilb9USv0vuX+nYzOWJp8lIujseJ/y3sU= github.com/goark/go-cvss v1.6.6/go.mod h1:H3qbfUSUlV7XtA3EwWNunvXz6OySwWHOuO+R6ZPMQPI= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -390,8 +385,8 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -github.com/google/osv-scanner v1.4.0 h1:9b97UMAD0FKZRpeYpZ5A9azsZZfsjDIx0qe24xxU1CU= -github.com/google/osv-scanner v1.4.0/go.mod h1:OZAKw86Wory8cAX9MFrvHdP7S85oJySGyD/jHxNSS+c= +github.com/google/osv-scanner v1.4.1 h1:fDddlRTtemqH0CmllRb56AUI7RvNWT0QduBNIZdTXzg= +github.com/google/osv-scanner v1.4.1/go.mod h1:QF4KFq59E2agCe2j9cXE5oGMsenjs8aL4yqZw81Rzhw= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -400,8 +395,8 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 h1:n6vlPhxsA+BW/XsS5+uqi7GyzaLa5MH7qlSLBZtRdiA= github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= -github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -463,6 +458,7 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab h1:BA4a7pe6ZTd9F8kXETBoijjFJ/ntaa//1wiH9BZu4zU= @@ -545,8 +541,9 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= github.com/mcuadros/go-jsonschema-generator v0.0.0-20200330054847-ba7a369d4303 h1:mc6Th1b2xkPDUHTIUynE0LMJUgPEJdIDUjBLvj8yprs= @@ -594,8 +591,8 @@ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI= -github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ= +github.com/onsi/ginkgo/v2 v2.12.1 h1:uHNEO1RP2SpuZApSkel9nEh1/Mu+hmQe7Q+Pepg5OYA= +github.com/onsi/ginkgo/v2 v2.12.1/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -603,20 +600,20 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/onsi/gomega v1.28.0 h1:i2rg/p9n/UqIDAMFUJ6qIUUMcsqOuUHgbpbu235Vr1c= +github.com/onsi/gomega v1.28.0/go.mod h1:A1H2JE76sI14WIP57LMKj7FVfCHx3g3BcZVjJG8bjX8= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= -github.com/otiai10/copy v1.12.0 h1:cLMgSQnXBs1eehF0Wy/FAGsgDTDmAqFR7rQylBb1nDY= -github.com/otiai10/copy v1.12.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww= +github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= +github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U= -github.com/owenrumney/go-sarif/v2 v2.2.0 h1:1DmZaijK0HBZCR1fgcDSGa7VzYkU9NDmbZ7qC2QfUjE= -github.com/owenrumney/go-sarif/v2 v2.2.0/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w= +github.com/owenrumney/go-sarif/v2 v2.2.2 h1:x2acaiiAW9hu+78wbEYBRGLk5nRtHmkv7HeUsKvblwc= +github.com/owenrumney/go-sarif/v2 v2.2.2/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w= github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU= github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -653,12 +650,13 @@ github.com/prometheus/prometheus v0.46.0 h1:9JSdXnsuT6YsbODEhSQMwxNkGwPExfmzqG73 github.com/prometheus/prometheus v0.46.0/go.mod h1:10L5IJE5CEsjee1FnOcVswYXlPIscDWWt3IJ2UDYrz4= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= -github.com/rhysd/actionlint v1.6.15 h1:IxQIp10aVce77jNnoHye7NFka8/7CRBSvKXoMRGryXM= -github.com/rhysd/actionlint v1.6.15/go.mod h1:R4ZRjgsIrnsT1CPU/4MdiIBzfJgMKJFd4qqGUERI098= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rhysd/actionlint v1.6.26 h1:zi7jPZf3Ks14gCXYAAL47uBziyFlX7+Xwilqhexct9g= +github.com/rhysd/actionlint v1.6.26/go.mod h1:TIj1DlCgtYLOv5CH9wCK+WJTOr1qAdnFzkGi0IgSCO4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= -github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -670,8 +668,8 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa h1:jozR3igKlnYCj9IVHOVump59bp07oIRoLQ/CcjMYIUA= github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a h1:KikTa6HtAK8cS1qjvUvvq4QO21QnwC+EfvB+OAuZ/ZU= @@ -686,8 +684,8 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= -github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= +github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= +github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -745,8 +743,8 @@ github.com/vdemeester/k8s-pkg-credentialprovider v1.18.1-0.20201019120933-f1d169 github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= -github.com/xanzy/go-gitlab v0.91.1 h1:gnV57IPGYywWer32oXKBcdmc8dVxeKl3AauV8Bu17rw= -github.com/xanzy/go-gitlab v0.91.1/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw= +github.com/xanzy/go-gitlab v0.93.1 h1:f7J33cw/P9b/8paIOoH0F3H+TFrswvWHs6yUgoTp9LY= +github.com/xanzy/go-gitlab v0.93.1/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= @@ -795,13 +793,12 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -886,15 +883,15 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= -golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -970,8 +967,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -982,8 +979,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -993,7 +990,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -1056,8 +1052,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/vuln v1.0.0 h1:tYLAU3jD9LQr98Y+3el06lWyGMCnvzw06PIWP3LIy7g= -golang.org/x/vuln v1.0.0/go.mod h1:V0eyhHwaAaHrt42J9bgrN6rd12f6GU4T0Lu0ex2wDg4= +golang.org/x/vuln v1.0.1 h1:KUas02EjQK5LTuIx1OylBQdKKZ9jeugs+HiqO5HormU= +golang.org/x/vuln v1.0.1/go.mod h1:bb2hMwln/tqxg32BNY4CcxHWtHXuYa3SbIBmtsyjxtM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1079,8 +1075,8 @@ google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.134.0 h1:ktL4Goua+UBgoP1eL1/60LwZJqa1sIzkLmvoR3hR6Gw= -google.golang.org/api v0.134.0/go.mod h1:sjRL3UnjTx5UqNQS9EWr9N8p7xbHpy1k0XGRLCf3Spk= +google.golang.org/api v0.139.0 h1:A1TrCPgMmOiYu0AiNkvQIpIx+D8blHTDcJ5EogkP7LI= +google.golang.org/api v0.139.0/go.mod h1:CVagp6Eekz9CjGZ718Z+sloknzkDJE7Vc1Ckj9+viBk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1112,12 +1108,12 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20201203001206-6486ece9c497/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20230731193218-e0aa005b6bdf h1:v5Cf4E9+6tawYrs/grq1q1hFpGtzlGFzgWHqwt6NFiU= -google.golang.org/genproto v0.0.0-20230731193218-e0aa005b6bdf/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= -google.golang.org/genproto/googleapis/api v0.0.0-20230731193218-e0aa005b6bdf h1:xkVZ5FdZJF4U82Q/JS+DcZA83s/GRVL+QrFMlexk9Yo= -google.golang.org/genproto/googleapis/api v0.0.0-20230731193218-e0aa005b6bdf/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230731193218-e0aa005b6bdf h1:guOdSPaeFgN+jEJwTo1dQ71hdBm+yKSCCKuTRkJzcVo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230731193218-e0aa005b6bdf/go.mod h1:zBEcrKX2ZOcEkHWxBPAIvYUWOKKMIhYcmNiUIu2ji3I= +google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g= +google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= +google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 h1:nIgk/EEq3/YlnmVVXVnm14rC2oxgs1o0ong4sD/rd44= +google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1133,7 +1129,6 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/options/flags.go b/options/flags.go index 7c974f02971..2691c4a1978 100644 --- a/options/flags.go +++ b/options/flags.go @@ -63,6 +63,12 @@ const ( // FlagFormat is the flag name for specifying output format. FlagFormat = "format" + // FlagResultsFile is the flag name for specifying output file. + FlagResultsFile = "output" + + // ShorthandFlagResultsFile is the shorthand flag name for specifying output file. + ShorthandFlagResultsFile = "o" + FlagCommitDepth = "commit-depth" ) @@ -188,4 +194,12 @@ func (o *Options) AddFlags(cmd *cobra.Command) { strings.Join(allowedFormats, ", "), ), ) + + cmd.Flags().StringVarP( + &o.ResultsFile, + FlagResultsFile, + ShorthandFlagResultsFile, + o.ResultsFile, + "output file", + ) } diff --git a/options/flags_test.go b/options/flags_test.go index ff4a38b3e4d..bf20642ecf8 100644 --- a/options/flags_test.go +++ b/options/flags_test.go @@ -43,6 +43,7 @@ func TestOptions_AddFlags(t *testing.T) { ChecksToRun: []string{"check1", "check2"}, PolicyFile: "policy-file", Format: "json", + ResultsFile: "result.json", }, }, } @@ -102,6 +103,18 @@ func TestOptions_AddFlags(t *testing.T) { if cmd.Flag(FlagFormat).Value.String() != tt.opts.Format { t.Errorf("expected FlagFormat to be %q, but got %q", tt.opts.Format, cmd.Flag(FlagFormat).Value.String()) } + + // check FlagResultsFile + if cmd.Flag(FlagResultsFile).Value.String() != tt.opts.ResultsFile { + t.Errorf("expected FlagResultsFile to be %q, but got %q", tt.opts.ResultsFile, + cmd.Flag(FlagResultsFile).Value.String()) + } + + // check ShorthandFlagResultsFile + if cmd.Flag(FlagResultsFile).Shorthand != ShorthandFlagResultsFile { + t.Errorf("expected ShorthandFlagResultsFile to be %q, but got %q", ShorthandFlagResultsFile, + cmd.Flag(FlagResultsFile).Shorthand) + } }) } } diff --git a/options/options.go b/options/options.go index 5be1fda1feb..f4d528b44f8 100644 --- a/options/options.go +++ b/options/options.go @@ -29,17 +29,16 @@ import ( // Options define common options for configuring scorecard. type Options struct { - Repo string - Local string - Commit string - LogLevel string - Format string - NPM string - PyPI string - RubyGems string - Nuget string - PolicyFile string - // TODO(action): Add logic for writing results to file + Repo string + Local string + Commit string + LogLevel string + Format string + NPM string + PyPI string + RubyGems string + Nuget string + PolicyFile string ResultsFile string ChecksToRun []string Metadata []string diff --git a/pkg/json.go b/pkg/json.go index 3257e91dc6b..1382cc9f086 100644 --- a/pkg/json.go +++ b/pkg/json.go @@ -176,17 +176,18 @@ func (r *ScorecardResult) AsJSON2(showDetails bool, return nil } -// This function is experimental. Do not depend on it, it may be removed at any point. -func ExperimentalFromJSON2(r io.Reader) (ScorecardResult, error) { +// ExperimentalFromJSON2 is experimental. Do not depend on it, it may be removed at any point. +// Also returns the aggregate score, as the ScorecardResult field does not contain it. +func ExperimentalFromJSON2(r io.Reader) (result ScorecardResult, score float64, err error) { var jsr JSONScorecardResultV2 decoder := json.NewDecoder(r) if err := decoder.Decode(&jsr); err != nil { - return ScorecardResult{}, fmt.Errorf("decode json: %w", err) + return ScorecardResult{}, 0, fmt.Errorf("decode json: %w", err) } date, err := time.Parse(time.RFC3339, jsr.Date) if err != nil { - return ScorecardResult{}, fmt.Errorf("parse scorecard analysis time: %w", err) + return ScorecardResult{}, 0, fmt.Errorf("parse scorecard analysis time: %w", err) } sr := ScorecardResult{ @@ -216,7 +217,7 @@ func ExperimentalFromJSON2(r io.Reader) (ScorecardResult, error) { sr.Checks = append(sr.Checks, cr) } - return sr, nil + return sr, float64(jsr.AggregateScore), nil } func (r *ScorecardResult) AsFJSON(showDetails bool, diff --git a/pkg/scorecard.go b/pkg/scorecard.go index 6cca73482bd..b8c1a4e1691 100644 --- a/pkg/scorecard.go +++ b/pkg/scorecard.go @@ -127,6 +127,7 @@ func RunScorecard(ctx context.Context, } else if err != nil { return ScorecardResult{}, err } + ret.Repo.CommitSHA = commitSHA defaultBranch, err := repoClient.GetDefaultBranchName() if err != nil { diff --git a/pkg/scorecard_result.go b/pkg/scorecard_result.go index c30167c5293..6a005f94c98 100644 --- a/pkg/scorecard_result.go +++ b/pkg/scorecard_result.go @@ -111,20 +111,30 @@ func FormatResults( ) error { var err error + // Define output to console or file + output := os.Stdout + if opts.ResultsFile != "" { + output, err = os.Create(opts.ResultsFile) + if err != nil { + return fmt.Errorf("unable to create output file: %w", err) + } + defer output.Close() + } + switch opts.Format { case options.FormatDefault: - err = results.AsString(opts.ShowDetails, log.ParseLevel(opts.LogLevel), doc, os.Stdout) + err = results.AsString(opts.ShowDetails, log.ParseLevel(opts.LogLevel), doc, output) case options.FormatSarif: // TODO: support config files and update checker.MaxResultScore. - err = results.AsSARIF(opts.ShowDetails, log.ParseLevel(opts.LogLevel), os.Stdout, doc, policy, opts) + err = results.AsSARIF(opts.ShowDetails, log.ParseLevel(opts.LogLevel), output, doc, policy, opts) case options.FormatJSON: - err = results.AsJSON2(opts.ShowDetails, log.ParseLevel(opts.LogLevel), doc, os.Stdout) + err = results.AsJSON2(opts.ShowDetails, log.ParseLevel(opts.LogLevel), doc, output) case options.FormatFJSON: - err = results.AsFJSON(opts.ShowDetails, log.ParseLevel(opts.LogLevel), doc, os.Stdout) + err = results.AsFJSON(opts.ShowDetails, log.ParseLevel(opts.LogLevel), doc, output) case options.FormatPJSON: - err = results.AsPJSON(os.Stdout) + err = results.AsPJSON(output) case options.FormatRaw: - err = results.AsRawJSON(os.Stdout) + err = results.AsRawJSON(output) default: err = sce.WithMessage( sce.ErrScorecardInternal, @@ -195,10 +205,10 @@ func (r *ScorecardResult) AsString(showDetails bool, logLevel log.Level, if score == checker.InconclusiveResultScore { s = "Aggregate score: ?\n\n" } - fmt.Fprint(os.Stdout, s) - fmt.Fprintln(os.Stdout, "Check scores:") + fmt.Fprint(writer, s) + fmt.Fprintln(writer, "Check scores:") - table := tablewriter.NewWriter(os.Stdout) + table := tablewriter.NewWriter(writer) header := []string{"Score", "Name", "Reason"} if showDetails { header = append(header, "Details") diff --git a/pkg/scorecard_result_test.go b/pkg/scorecard_result_test.go new file mode 100644 index 00000000000..7d78aafa29c --- /dev/null +++ b/pkg/scorecard_result_test.go @@ -0,0 +1,184 @@ +// Copyright 2023 OpenSSF Scorecard Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pkg + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "testing" + "time" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/docs/checks" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/log" + "github.com/ossf/scorecard/v4/options" + spol "github.com/ossf/scorecard/v4/policy" +) + +func mockScorecardResultCheck1(t *testing.T) *ScorecardResult { + t.Helper() + // Helper variables to mock Scorecard results + date, e := time.Parse(time.RFC3339, "2023-03-02T10:30:43-06:00") + t.Logf("date: %v", date) + if e != nil { + panic(fmt.Errorf("time.Parse: %w", e)) + } + + return &ScorecardResult{ + Repo: RepoInfo{ + Name: "org/name", + CommitSHA: "68bc59901773ab4c051dfcea0cc4201a1567ab32", + }, + Scorecard: ScorecardInfo{ + Version: "1.2.3", + CommitSHA: "ccbc59901773ab4c051dfcea0cc4201a1567abdd", + }, + Date: date, + Checks: []checker.CheckResult{ + { + Details: []checker.CheckDetail{ + { + Type: checker.DetailWarn, + Msg: checker.LogMessage{ + Text: "warn message", + Path: "src/file1.cpp", + Type: finding.FileTypeSource, + Offset: 5, + Snippet: "if (bad) {BUG();}", + }, + }, + }, + Score: 5, + Reason: "half score reason", + Name: "Check-Name", + }, + }, + Metadata: []string{}, + } +} + +func Test_formatResults_outputToFile(t *testing.T) { + t.Parallel() + type args struct { + opts *options.Options + results *ScorecardResult + doc checks.Doc + policy *spol.ScorecardPolicy + } + type want struct { + path string + err bool + } + + // Helper variables to mock scorecard results and checks doc + scorecardResults := mockScorecardResultCheck1(t) + checkDocs := jsonMockDocRead() + + tests := []struct { + name string + args args + want want + }{ + { + name: "output file with format json", + args: args{ + opts: &options.Options{ + Format: options.FormatJSON, + ShowDetails: true, + LogLevel: log.DebugLevel.String(), + }, + results: scorecardResults, + doc: checkDocs, + }, + want: want{ + path: "check1.json", + err: false, + }, + }, + { + name: "output file with format default", + args: args{ + opts: &options.Options{ + Format: options.FormatDefault, + ShowDetails: true, + LogLevel: log.DebugLevel.String(), + }, + results: scorecardResults, + doc: checkDocs, + }, + want: want{ + path: "check1.log", + err: false, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // generate a unique result file in a temp directory for every test run to avoid race conditions in the test. + // This can happen when ginkgo runs unit tests in parallel (with -p flag) + resultFile, err := os.CreateTemp("", "result-file") + if err != nil { + t.Fatalf("create temp result file: %v", err) + } + tt.args.opts.ResultsFile = resultFile.Name() + + // Format results. + formatErr := FormatResults(tt.args.opts, tt.args.results, tt.args.doc, tt.args.policy) + // Delete generated output file at the end of the test. + t.Cleanup(func() { os.Remove(tt.args.opts.ResultsFile) }) + if (formatErr != nil) != tt.want.err { + t.Fatalf("FormatResults() error = %v, want error %v", formatErr, tt.want.err) + } + + // Get output and wanted output. + output, outputErr := os.ReadFile(tt.args.opts.ResultsFile) + if outputErr != nil { + t.Fatalf("cannot read file: %v", outputErr) + } + wantOutput, wantOutputErr := os.ReadFile("./testdata/" + tt.want.path) + if wantOutputErr != nil { + t.Fatalf("cannot read file: %v", wantOutputErr) + } + + // Unmarshal if comparing JSON output. + if tt.args.opts.Format == options.FormatJSON { + // Unmarshal expected output. + var js JSONScorecardResultV2 + if err := json.Unmarshal(wantOutput, &js); err != nil { + t.Fatalf("%s: json.Unmarshal: %s", tt.name, err) + } + + // Marshal. + var es bytes.Buffer + encoder := json.NewEncoder(&es) + if err := encoder.Encode(js); err != nil { + t.Fatalf("%s: Encode: %s", tt.name, err) + } + wantOutput = es.Bytes() + } + + // Compare outputs. + if !bytes.Equal(output, wantOutput) { + t.Errorf("%v\nGOT\n-------\n%s\nWANT\n-------\n%s", tt.name, string(output), string(wantOutput)) + } + }) + } +} diff --git a/pkg/testdata/check1.log b/pkg/testdata/check1.log new file mode 100644 index 00000000000..c91b9c8bdfd --- /dev/null +++ b/pkg/testdata/check1.log @@ -0,0 +1,9 @@ +Aggregate score: 5.0 / 10 + +Check scores: +|--------|------------|-------------------|--------------------------------|-----------------------------------------------------------------------| +| SCORE | NAME | REASON | DETAILS | DOCUMENTATION/REMEDIATION | +|--------|------------|-------------------|--------------------------------|-----------------------------------------------------------------------| +| 5 / 10 | Check-Name | half score reason | Warn: warn message: | https://github.com/ossf/scorecard/blob/main/docs/checks.md#check-name | +| | | | src/file1.cpp:5 | | +|--------|------------|-------------------|--------------------------------|-----------------------------------------------------------------------| diff --git a/probes/internal/utils/test/test.go b/probes/internal/utils/test/test.go new file mode 100644 index 00000000000..f63c243b534 --- /dev/null +++ b/probes/internal/utils/test/test.go @@ -0,0 +1,44 @@ +// Copyright 2023 OpenSSF Scorecard Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/ossf/scorecard/v4/finding" +) + +// AssertCorrect is a suitable for all probes to compare raw +// results against expected outcomes. +func AssertCorrect(t *testing.T, probeExpect, probeGot string, + findings []finding.Finding, expectedOutcomes []finding.Outcome, +) { + t.Helper() + if diff := cmp.Diff(probeExpect, probeGot); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff(len(expectedOutcomes), len(findings)); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + for i := range expectedOutcomes { + outcome := &expectedOutcomes[i] + f := &findings[i] + if diff := cmp.Diff(*outcome, f.Outcome); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + } +} diff --git a/tools/go.mod b/tools/go.mod index fcc1cfb0da0..9bfc64d538c 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -9,7 +9,7 @@ require ( github.com/google/ko v0.14.1 github.com/goreleaser/goreleaser v1.20.0 github.com/naveensrinivasan/stunning-tribble v0.4.2 - github.com/onsi/ginkgo/v2 v2.12.0 + github.com/onsi/ginkgo/v2 v2.12.1 google.golang.org/protobuf v1.31.0 ) @@ -362,7 +362,7 @@ require ( golang.org/x/net v0.14.0 // indirect golang.org/x/oauth2 v0.11.0 // indirect golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.11.0 // indirect + golang.org/x/sys v0.12.0 // indirect golang.org/x/term v0.11.0 // indirect golang.org/x/text v0.12.0 // indirect golang.org/x/time v0.3.0 // indirect diff --git a/tools/go.sum b/tools/go.sum index 45196ed083b..1f7f11c22bd 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -884,8 +884,8 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI= -github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ= +github.com/onsi/ginkgo/v2 v2.12.1 h1:uHNEO1RP2SpuZApSkel9nEh1/Mu+hmQe7Q+Pepg5OYA= +github.com/onsi/ginkgo/v2 v2.12.1/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= @@ -1388,8 +1388,8 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=