├── .dockerignore ├── .gcloudignore ├── .github ├── dependabot.yml └── workflows │ ├── binary-release.yml │ ├── ci.yml │ ├── codeql-analysis.yml │ └── scorecards.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── CITATION.cff ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── Quantifying_criticality_algorithm.pdf ├── README.md ├── SECURITY.md ├── cmd ├── collect_signals │ ├── Dockerfile │ ├── README.md │ ├── localworker │ │ ├── loop.go │ │ ├── loop_test.go │ │ ├── runstate.go │ │ ├── runstate_unix.go │ │ ├── runstate_windows.go │ │ └── scorecardcompat.go │ ├── main.go │ ├── vcs │ │ └── vcs.go │ └── worker.go ├── criticality_score │ ├── Dockerfile │ ├── README.md │ ├── inputiter │ │ ├── err_windows.go │ │ ├── new.go │ │ └── new_test.go │ ├── main.go │ └── version.go ├── csv_transfer │ ├── Dockerfile │ ├── README.md │ └── main.go ├── enumerate_github │ ├── Dockerfile │ ├── README.md │ ├── githubsearch │ │ ├── repos.go │ │ └── search.go │ ├── main.go │ ├── marker │ │ ├── type.go │ │ ├── type_test.go │ │ ├── write.go │ │ └── write_test.go │ └── repowriter │ │ ├── scorecard.go │ │ ├── scorecard_test.go │ │ ├── text.go │ │ ├── text_test.go │ │ ├── type.go │ │ ├── type_test.go │ │ └── writer.go └── scorer │ ├── README.md │ ├── main.go │ └── pq.go ├── config └── scorer │ ├── original_pike.yml │ └── pike_depsdev.yml ├── docs ├── CHARTER.md ├── design │ ├── images │ │ └── github-enumeration-infra.svg │ ├── milestone_1.md │ └── milestone_2.md └── glossary.md ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── images └── formula.png ├── infra ├── README.md ├── cloudbuild │ ├── collect_signals │ │ └── cloudbuild.yaml │ ├── csv_transfer │ │ └── cloudbuild.yaml │ ├── enumerate_github │ │ └── cloudbuild.yaml │ ├── init_collect_signals │ │ └── cloudbuild.yaml │ └── release.yaml ├── clouddeploy.yaml ├── envs │ ├── base │ │ ├── bq_transfer.yaml │ │ ├── csv_transfer.yaml │ │ ├── kustomization.yaml │ │ └── worker.yaml │ ├── prod │ │ ├── config.yaml │ │ ├── github_auth.yaml │ │ └── kustomization.yaml │ └── staging │ │ ├── bq_transfer_schedule.yaml │ │ ├── config.yaml │ │ ├── controller_schedule.yaml │ │ ├── csv_transfer_schedule.yaml │ │ ├── github_auth.yaml │ │ └── kustomization.yaml ├── images │ └── init_collect_signals │ │ ├── Dockerfile │ │ └── init.sh ├── k8s │ └── enumerate_github.yaml ├── skaffold.yaml └── test │ ├── README.md │ └── docker-compose.yml ├── internal ├── cloudstorage │ ├── cloudstorage.go │ └── cloudstorage_test.go ├── collector │ ├── collector.go │ ├── config.go │ ├── config_test.go │ ├── depsdev │ │ ├── bq.go │ │ ├── dependents.go │ │ └── source.go │ ├── github │ │ ├── factory.go │ │ ├── legacy │ │ │ ├── constants.go │ │ │ ├── contributors.go │ │ │ ├── created.go │ │ │ ├── issues.go │ │ │ ├── releases.go │ │ │ └── util.go │ │ ├── queries.go │ │ ├── repo.go │ │ └── source.go │ ├── githubmentions │ │ └── source.go │ ├── projectrepo │ │ ├── repo.go │ │ └── resolver.go │ ├── registry.go │ └── signal │ │ ├── issues.go │ │ ├── repo.go │ │ ├── signal.go │ │ └── source.go ├── envflag │ ├── envflag.go │ └── envflag_test.go ├── githubapi │ ├── batch.go │ ├── client.go │ ├── errors.go │ ├── errors_test.go │ ├── graphql.go │ ├── pagination │ │ └── pagination.go │ ├── roundtripper.go │ └── roundtripper_test.go ├── infile │ ├── infile.go │ └── infile_test.go ├── iterator │ ├── batch.go │ ├── batch_test.go │ ├── iterator.go │ ├── scanner.go │ ├── scanner_test.go │ ├── slice.go │ └── slice_test.go ├── log │ ├── config.go │ ├── env.go │ └── env_test.go ├── outfile │ ├── outfile.go │ └── outfile_test.go ├── retry │ ├── request.go │ └── request_test.go ├── scorer │ ├── algorithm │ │ ├── algorithm.go │ │ ├── distribution.go │ │ ├── distribution_test.go │ │ ├── input.go │ │ ├── input_test.go │ │ ├── registry.go │ │ ├── value.go │ │ ├── value_test.go │ │ └── wam │ │ │ ├── wam.go │ │ │ └── wam_test.go │ ├── config.go │ ├── config_test.go │ ├── default.go │ ├── default_config.yml │ ├── scorer.go │ ├── scorer_test.go │ └── testdata │ │ ├── default_config.yml │ │ └── invalid_config.yaml ├── signalio │ ├── csv.go │ ├── helpers.go │ ├── helpers_test.go │ ├── json.go │ ├── text.go │ ├── type.go │ ├── type_test.go │ └── writer.go └── workerpool │ ├── workerpool.go │ └── workerpool_test.go ├── output └── .gitignore ├── popularity_correlation.md ├── scripts └── validate-scorecard-version.sh └── tools ├── go.mod ├── go.sum └── tools.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore irrelevant files 2 | .github 3 | infra 4 | docs 5 | images 6 | 7 | # Ignore Dockerfile - this improve caching. 8 | **/Dockerfile 9 | 10 | !infra/images 11 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | docs 4 | images 5 | *.md 6 | *.pdf 7 | *.py 8 | 9 | #!include:.gitignore -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: gomod 9 | directory: "/tools" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 5 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "daily" 17 | -------------------------------------------------------------------------------- /.github/workflows/binary-release.yml: -------------------------------------------------------------------------------- 1 | name: Binary Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | 12 | jobs: 13 | goreleaser: 14 | runs-on: ubuntu-latest 15 | outputs: 16 | hashes: ${{ steps.hash.outputs.hashes }} 17 | permissions: 18 | contents: write # needed for goreleaser to create the release 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 27 | with: 28 | go-version-file: 'go.mod' 29 | 30 | - name: Run GoReleaser 31 | id: run-goreleaser 32 | uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 33 | with: 34 | distribution: goreleaser 35 | version: latest 36 | args: release --clean 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | - name: Generate subject 41 | id: hash 42 | env: 43 | ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}" 44 | run: | 45 | set -euo pipefail 46 | checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path') 47 | echo "hashes=$(cat $checksum_file | base64 -w0)" >> "$GITHUB_OUTPUT" 48 | 49 | provenance: 50 | needs: [goreleaser] 51 | permissions: 52 | actions: read # To read the workflow path. 53 | id-token: write # To sign the provenance. 54 | contents: write # To add assets to a release. 55 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 56 | with: 57 | base64-subjects: "${{ needs.goreleaser.outputs.hashes }}" 58 | upload-assets: true # upload to a new release 59 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Continuous Integration" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | run-tests: 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 19 | - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 20 | with: 21 | go-version-file: 'go.mod' 22 | - name: Run tests 23 | run: make test/unit 24 | env: 25 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | run-linter: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 30 | - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 31 | with: 32 | go-version-file: 'tools/go.mod' 33 | - name: golangci-lint 34 | uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd 35 | with: 36 | args: --timeout=30m 37 | check-scorecard-version: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 41 | - name: Run tests 42 | run: make test/scorecard-version 43 | env: 44 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '36 22 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'go' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3.26.6 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v3.26.6 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v3.26.6 68 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | name: OSSF Scorecard 2 | on: 3 | push: 4 | # Only the default branch is supported. 5 | branches: 6 | - main 7 | schedule: 8 | # Weekly on Saturdays. 9 | - cron: '30 1 * * 6' 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | analysis: 15 | name: Scorecard analysis 16 | runs-on: ubuntu-latest 17 | permissions: 18 | security-events: write 19 | id-token: write 20 | steps: 21 | - name: "Checkout code" 22 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 23 | - name: "Run analysis" 24 | uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.0.0-alpha.2 25 | with: 26 | results_file: results.sarif 27 | results_format: sarif 28 | repo_token: ${{ secrets.GITHUB_TOKEN }} 29 | # Publish the results for public repositories to enable scorecard badges. For more details, see 30 | # https://github.com/ossf/scorecard-action#publishing-results. 31 | # For private repositories, `publish_results` will automatically be set to `false`, regardless 32 | # of the value entered here. 33 | publish_results: true 34 | # https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts 35 | # Optional. 36 | - name: "Upload artifact" 37 | uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v2 38 | with: 39 | name: SARIF file 40 | path: results.sarif 41 | retention-days: 5 42 | - name: "Upload SARIF results" 43 | uses: github/codeql-action/upload-sarif@b43ac1c23f7541b1939bccfc4255db76ae3cd3dd # v1 44 | with: 45 | sarif_file: results.sarif 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rope project settings 2 | .ropeproject 3 | 4 | # This is for IDEs/Editors 5 | .idea 6 | 7 | # Skaffold dir 8 | .kpt-* 9 | .swp 10 | 11 | # Test output 12 | unit-coverage.out 13 | 14 | dist/ 15 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "2" 3 | run: 4 | concurrency: 6 5 | timeout: 5m 6 | issues: 7 | # Maximum issues count per one linter. 8 | # Set to 0 to disable. 9 | # Default: 50 10 | max-issues-per-linter: 0 11 | # Maximum count of issues with the same text. 12 | # Set to 0 to disable. 13 | # Default: 3 14 | max-same-issues: 0 15 | # Exclude previously existing issues from the report 16 | new: true 17 | new-from-rev: HEAD 18 | 19 | formatters: 20 | settings: 21 | gci: 22 | sections: 23 | - standard 24 | - default 25 | - prefix(github.com/ossf/criticality_score) 26 | 27 | linters: 28 | default: none 29 | enable: 30 | - asciicheck 31 | #- bodyclose # Temporarily disabled. 32 | #- depguard # Temporarily disabled. 33 | - copyloopvar 34 | - dogsled 35 | #- errcheck # Temporarily disabled. 36 | - errorlint 37 | - exhaustive 38 | #- gochecknoinits # Temporarily disabled. 39 | - gocognit 40 | - goconst 41 | - gocritic 42 | - gocyclo 43 | - godot 44 | - godox 45 | #- goerr113 # Temporarily disabled. 46 | - goheader 47 | - gomodguard 48 | - goprintffuncname 49 | - gosec 50 | - govet 51 | - ineffassign 52 | #- lll # Temporarily disabled. 53 | - makezero 54 | - misspell 55 | - nakedret 56 | - nestif 57 | - noctx 58 | - nolintlint 59 | - predeclared 60 | - staticcheck 61 | - thelper 62 | - tparallel 63 | - unconvert 64 | - unparam 65 | - unused 66 | - whitespace 67 | - wrapcheck 68 | settings: 69 | errcheck: 70 | check-type-assertions: true 71 | check-blank: true 72 | exhaustive: 73 | # https://golangci-lint.run/usage/linters/#exhaustive 74 | default-signifies-exhaustive: true 75 | godot: 76 | exclude: 77 | - "^ -- " 78 | govet: 79 | enable: 80 | - fieldalignment 81 | godox: 82 | keywords: 83 | - BUG 84 | - FIXME 85 | - HACK 86 | gocritic: 87 | enabled-checks: 88 | # Diagnostic 89 | - appendAssign 90 | - badCond 91 | - caseOrder 92 | - codegenComment 93 | - commentedOutCode 94 | - deprecatedComment 95 | - dupBranchBody 96 | - dupCase 97 | - dupSubExpr 98 | #- exitAfterDefer # Temporarily disabled 99 | - flagName 100 | - nilValReturn 101 | - weakCond 102 | - octalLiteral 103 | 104 | # Performance 105 | - appendCombine 106 | #- hugeParam # Crashing. 107 | - rangeExprCopy 108 | - rangeValCopy 109 | 110 | # Style 111 | - boolExprSimplify 112 | - captLocal 113 | - commentFormatting 114 | - commentedOutImport 115 | - defaultCaseOrder 116 | - docStub 117 | - elseif 118 | - emptyFallthrough 119 | - hexLiteral 120 | - ifElseChain 121 | - methodExprCall 122 | - singleCaseSwitch 123 | - typeAssertChain 124 | - typeSwitchVar 125 | - underef 126 | - unlabelStmt 127 | - unlambda 128 | 129 | # Opinionated 130 | - builtinShadow 131 | - importShadow 132 | - initClause 133 | - nestingReduce 134 | - paramTypeCombine 135 | - ptrToRefParam 136 | - typeUnparen 137 | - unnecessaryBlock 138 | 139 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: criticality_score 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | 7 | builds: 8 | - main: ./cmd/criticality_score 9 | id: "criticality_score" 10 | binary: criticality_score 11 | flags: 12 | - -buildvcs 13 | env: 14 | - CGO_ENABLED=0 15 | 16 | - main: ./cmd/enumerate_github 17 | id: "enumerate_github" 18 | binary: enumerate_github 19 | env: 20 | - CGO_ENABLED=0 21 | 22 | - main: ./cmd/scorer 23 | id: "scorer" 24 | binary: scorer 25 | env: 26 | - CGO_ENABLED=0 27 | 28 | archives: 29 | - id: tarballs 30 | format: tar.gz 31 | format_overrides: 32 | - goos: windows 33 | format: zip 34 | files: 35 | - LICENSE 36 | - README.md 37 | - src: cmd/criticality_score/README.md 38 | dst: README_criticality_score.md 39 | - src: cmd/enumerate_github/README.md 40 | dst: README_enumerate_github.md 41 | - src: cmd/scorer/README.md 42 | dst: README_scorer.md 43 | rlcp: true 44 | 45 | checksum: 46 | name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt' 47 | 48 | snapshot: 49 | name_template: "{{ incpatch .Version }}-next" 50 | 51 | changelog: 52 | skip: true 53 | 54 | release: 55 | draft: false 56 | 57 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 58 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 59 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | type: software 2 | cff-version: 1.2.0 3 | message: "If you use this software, please cite it as below." 4 | authors: 5 | - family-names: "Arya" 6 | given-names: "Abhishek" 7 | - family-names: "Brown" 8 | given-names: "Caleb" 9 | - family-names: "Pike" 10 | given-names: "Rob" 11 | - name: "The Open Source Security Foundation" 12 | title: "Open Source Project Criticality Score" 13 | date-released: 2023-03-01 14 | url: "https://github.com/ossf/criticality_score" 15 | license: Apache-2.0 16 | version: "2.0.2" 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to OSS Criticality Score! 2 | 3 | Thank you for contributing your time and expertise to the OSS Criticality Score 4 | project. This document describes the contribution guidelines for the project. 5 | 6 | **Note:** Before you start contributing, you must read and abide by our 7 | **[Code of Conduct](./CODE_OF_CONDUCT.md)**. 8 | 9 | ## Contributing code 10 | 11 | ### Getting started 12 | 13 | 1. Create [a GitHub account](https://github.com/join) 14 | 1. Create a [personal access token](https://docs.github.com/en/free-pro-team@latest/developers/apps/about-apps#personal-access-tokens) 15 | 1. (Optionally) a Google Cloud Platform account for [deps.dev](https://deps.dev) data 16 | 1. Set up your [development environment](#environment-setup) 17 | 18 | Then you can [iterate](#iterating). 19 | 20 | ## Environment Setup 21 | 22 | You must install these tools: 23 | 24 | 1. [`git`](https://help.github.com/articles/set-up-git/): For source control. 25 | 26 | 1. [`go`](https://go.dev/dl/): For running code. 27 | 28 | And optionally: 29 | 30 | 1. [`gcloud`](https://cloud.google.com/sdk/docs/install): For Google Cloud Platform access for deps.dev data. 31 | 32 | Then clone the repository, e.g: 33 | 34 | ```shell 35 | $ git clone git@github.com:ossf/criticality_score.git 36 | $ cd criticality_score 37 | ``` 38 | 39 | ## Iterating 40 | 41 | 1. Find or create an [issue](https://github.com/ossf/criticality_score/issues) 42 | 43 | 1. Make code changes to: 44 | - the [collect_signals CLI tool](https://github.com/ossf/criticality_score/tree/main/cmd/collect_signals) 45 | - the [GitHub enumerator](https://github.com/ossf/criticality_score/tree/main/cmd/enumerate_github) 46 | - the [signal collector worker](https://github.com/ossf/criticality_score/tree/main/cmd/collect_signals) 47 | - the [scorer](https://github.com/ossf/criticality_score/tree/main/cmd/scorer) 48 | - the scorer [algorithm configuration](https://github.com/ossf/criticality_score/tree/main/config/scorer) 49 | 50 | 1. Run your changes. For example, for a single repository this can be done by 51 | executing: 52 | 53 | ```shell 54 | $ export GITHUB_TOKEN=ghp_x # the personal access token created above 55 | $ go run ./cmd/criticality_score \ 56 | -log=debug \ 57 | -depsdev-disable \ # remove if you have a GCP account configured 58 | "https://github.com/{ a repo }" 59 | ``` 60 | Note: Each of the tools listed above can be run individually and has their own 61 | README. 62 | 63 | 4. Ensure your code passes tests and lint checks: 64 | 65 | ```shell 66 | $ make test 67 | $ make lint 68 | ``` 69 | 70 | 5. Commit your change. Upload to a fork, and create a pull request! -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | IMAGE_NAME = criticality-score 15 | GOLANGCI_LINT := golangci-lint 16 | GOFUMPT := gofumpt 17 | 18 | default: help 19 | 20 | .PHONY: help 21 | help: ## Display this help 22 | @awk 'BEGIN {FS = ":.*##"; \ 23 | printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9\/-]+:.*?##/ \ 24 | { printf " \033[36m%-25s\033[0m %s\n", $$1, $$2 } \ 25 | /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 26 | 27 | test-targets = test/unit test/scorecard-version 28 | .PHONY: test $(test-targets) 29 | test: $(test-targets) ## Run all tests 30 | test/unit: 31 | go test -race -covermode=atomic -coverprofile=unit-coverage.out './...' 32 | test/scorecard-version: 33 | bash ./scripts/validate-scorecard-version.sh 34 | 35 | .PHONY: lint 36 | $(GOLANGCI_LINT): install/deps 37 | lint: ## Run linter 38 | lint: $(GOLANGCI_LINT) 39 | $(GOLANGCI_LINT) run -c .golangci.yml 40 | 41 | .PHONY: format 42 | $(GOFUMPT): install/deps 43 | format: ## Run formatter 44 | format: $(GOFUMPT) 45 | $(GOFUMPT) -w -l . 46 | 47 | docker-targets = build/docker/enumerate-github build/docker/criticality-score build/docker/collect-signals build/docker/csv-transfer 48 | .PHONY: build/docker $(docker-targets) 49 | build/docker: $(docker-targets) ## Build all docker targets 50 | build/docker/collect-signals: 51 | DOCKER_BUILDKIT=1 docker build . -f cmd/collect_signals/Dockerfile --tag $(IMAGE_NAME)-collect-signals 52 | build/docker/criticality-score: 53 | DOCKER_BUILDKIT=1 docker build . -f cmd/criticality_score/Dockerfile --tag $(IMAGE_NAME)-cli 54 | build/docker/enumerate-github: 55 | DOCKER_BUILDKIT=1 docker build . -f cmd/enumerate_github/Dockerfile --tag $(IMAGE_NAME)-enumerate-github 56 | build/docker/csv-transfer: 57 | DOCKER_BUILDKIT=1 docker build . -f cmd/csv_transfer/Dockerfile --tag $(IMAGE_NAME)-csv-transfer 58 | 59 | .PHONY: install/deps 60 | install/deps: ## Installs all dependencies during development and building 61 | @echo Installing tools from tools/tools.go 62 | @cd tools; cat tools.go | grep _ | awk -F'"' '{print $$2}' | xargs -tI % go install % -------------------------------------------------------------------------------- /Quantifying_criticality_algorithm.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ossf/criticality_score/47238f446aac2e6ce83b2597c8feb58f156a9bb1/Quantifying_criticality_algorithm.pdf -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | To report a security issue, please email 4 | [oss-security@googlegroups.com](mailto:oss-security@googlegroups.com) 5 | with a description of the issue, the steps you took to create the issue, 6 | affected versions, and, if known, mitigations for the issue. 7 | 8 | Our vulnerability management team will respond within 3 working days of your 9 | email. If the issue is confirmed as a vulnerability, we will open a 10 | Security Advisory and acknowledge your contributions as part of it. This project 11 | follows a 90 day disclosure timeline. 12 | -------------------------------------------------------------------------------- /cmd/collect_signals/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM golang:1.24.2@sha256:1ecc479bc712a6bdb56df3e346e33edcc141f469f82840bab9f4bc2bc41bf91d AS base 16 | WORKDIR /src 17 | ENV CGO_ENABLED=0 18 | COPY go.mod go.sum ./ 19 | RUN go mod download 20 | COPY . ./ 21 | 22 | FROM base AS collect_signals 23 | ARG TARGETOS 24 | ARG TARGETARCH 25 | RUN CGO_ENABLED=0 go build -buildvcs ./cmd/collect_signals 26 | RUN chmod -R 0775 /src/config/scorer/* 27 | 28 | FROM gcr.io/distroless/base:nonroot@sha256:fa5f94fa433728f8df3f63363ffc8dec4adcfb57e4d8c18b44bceccfea095ebc 29 | COPY --from=collect_signals /src/collect_signals ./collect_signals 30 | COPY --from=collect_signals /src/config/scorer/* ./config/scorer/ 31 | ENTRYPOINT ["./collect_signals"] 32 | -------------------------------------------------------------------------------- /cmd/collect_signals/README.md: -------------------------------------------------------------------------------- 1 | # Signal Collector 2 | 3 | This tool is used to collect signal data for a set of project repositories for 4 | generating a criticality score. It is intended to be used as part of a pool of 5 | workers collecting signals for hundreds of thousands of repositories. 6 | -------------------------------------------------------------------------------- /cmd/collect_signals/localworker/runstate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package localworker 16 | 17 | import ( 18 | "encoding/json" 19 | "errors" 20 | "fmt" 21 | "os" 22 | "time" 23 | ) 24 | 25 | type runState struct { 26 | JobTime time.Time `json:"job-time"` 27 | filename string `json:"-"` 28 | Attempt int `json:"attempt"` 29 | Shard int32 `json:"shard"` 30 | } 31 | 32 | func loadState(filename string) (*runState, error) { 33 | f, err := os.Open(filename) 34 | if errors.Is(err, os.ErrNotExist) { 35 | return &runState{ 36 | JobTime: time.Now().UTC(), 37 | filename: filename, 38 | }, nil 39 | } else if err != nil { 40 | return nil, fmt.Errorf("open file %q: %w", filename, err) 41 | } 42 | defer f.Close() 43 | d := json.NewDecoder(f) 44 | var s *runState 45 | if err := d.Decode(&s); err != nil { 46 | return nil, fmt.Errorf("decoding json: %w", err) 47 | } 48 | s.filename = filename 49 | return s, nil 50 | } 51 | 52 | func (s *runState) Clear() error { 53 | if s.filename == "" { 54 | return fmt.Errorf("run state cleared") 55 | } 56 | f := s.filename 57 | s.filename = "" 58 | 59 | if err := os.Remove(f); errors.Is(err, os.ErrNotExist) { 60 | return nil 61 | } else if err != nil { 62 | return fmt.Errorf("removing file: %w", err) 63 | } 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /cmd/collect_signals/localworker/runstate_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build unix || (js && wasm) || wasip1 16 | 17 | package localworker 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | 23 | "github.com/google/renameio/v2" 24 | ) 25 | 26 | func (s *runState) Save() error { 27 | if s.filename == "" { 28 | return fmt.Errorf("run state cleared") 29 | } 30 | 31 | pf, err := renameio.NewPendingFile(s.filename) 32 | if err != nil { 33 | return fmt.Errorf("pending file: %w", err) 34 | } 35 | defer pf.Cleanup() 36 | 37 | e := json.NewEncoder(pf) 38 | if err := e.Encode(s); err != nil { 39 | return fmt.Errorf("encoding json: %w", err) 40 | } 41 | 42 | if err := pf.CloseAtomicallyReplace(); err != nil { 43 | return fmt.Errorf("atomic close: %w", err) 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /cmd/collect_signals/localworker/runstate_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package localworker 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "os" 21 | "path/filepath" 22 | ) 23 | 24 | func (s *runState) Save() error { 25 | if s.filename == "" { 26 | return fmt.Errorf("run state cleared") 27 | } 28 | 29 | dir := filepath.Dir(s.filename) 30 | base := filepath.Base(s.filename) 31 | 32 | // Temporary filename is based on the destination filename to ensure the 33 | // temp file is placed on the same volume as the destination file. This 34 | // avoids potential errors when calling os.Rename. 35 | tmp, err := os.CreateTemp(dir, fmt.Sprintf("%s-tmp-*", base)) 36 | if err != nil { 37 | return fmt.Errorf("temp file create: %w", err) 38 | } 39 | tmpFilename := tmp.Name() 40 | defer func() { 41 | // tmp is set to nil if close is called eariler. This avoids attempting 42 | // to close the temporary file more than once. 43 | if tmp != nil { 44 | tmp.Close() 45 | } 46 | // tmpFilename is set to the emptry string after the rename to prevent 47 | // remove being called. It is possible that another temp file with the 48 | // same name has taken its place by the time this has been called. 49 | if tmpFilename != "" { 50 | os.Remove(tmpFilename) 51 | } 52 | }() 53 | 54 | e := json.NewEncoder(tmp) 55 | if err := e.Encode(s); err != nil { 56 | return fmt.Errorf("encoding json: %w", err) 57 | } 58 | 59 | // Ensure the file's bytes have been written to storage and aren't in a cache. 60 | if err := tmp.Sync(); err != nil { 61 | return fmt.Errorf("temp file sync: %w", err) 62 | } 63 | 64 | err = tmp.Close() 65 | // Clear tmp to prevent the defer func from calling Close. 66 | tmp = nil 67 | if err != nil { 68 | return fmt.Errorf("temp file closing: %w", err) 69 | } 70 | 71 | if err := os.Rename(tmpFilename, s.filename); err != nil { 72 | return fmt.Errorf("rename temp %s -> %s: %w", tmpFilename, s.filename, err) 73 | } 74 | // Clear tmpFilename to prevent the defer func from calling Remove. 75 | tmpFilename = "" 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /cmd/collect_signals/localworker/scorecardcompat.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package localworker 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "time" 21 | 22 | "github.com/ossf/scorecard/v4/clients" 23 | "github.com/ossf/scorecard/v4/cron/data" 24 | "github.com/ossf/scorecard/v4/cron/worker" 25 | "google.golang.org/protobuf/encoding/protojson" 26 | "google.golang.org/protobuf/proto" 27 | "google.golang.org/protobuf/types/known/timestamppb" 28 | 29 | "github.com/ossf/criticality_score/v2/cmd/collect_signals/vcs" 30 | ) 31 | 32 | var headSHA = clients.HeadSHA 33 | 34 | func writeMetadata(ctx context.Context, bucketURL string, lastShard int32, t time.Time) error { 35 | // Populate `.shard_metadata` file. 36 | metadata := data.ShardMetadata{ 37 | NumShard: new(int32), 38 | ShardLoc: new(string), 39 | CommitSha: new(string), 40 | } 41 | *metadata.NumShard = lastShard + 1 42 | *metadata.ShardLoc = bucketURL + "/" + data.GetBlobFilename("", t) 43 | *metadata.CommitSha = vcs.CommitID() 44 | 45 | metadataJSON, err := protojson.Marshal(&metadata) 46 | if err != nil { 47 | return fmt.Errorf("protojson marshal: %w", err) 48 | } 49 | 50 | if err := data.WriteToBlobStore(ctx, bucketURL, data.GetShardMetadataFilename(t), metadataJSON); err != nil { 51 | return fmt.Errorf("writing to blob store: %w", err) 52 | } 53 | return nil 54 | } 55 | 56 | func makeRequest(repos []string, shardNum int32, t time.Time) *data.ScorecardBatchRequest { 57 | req := &data.ScorecardBatchRequest{ 58 | JobTime: timestamppb.New(t), 59 | ShardNum: proto.Int32(shardNum), 60 | } 61 | for _, repo := range repos { 62 | localRepo := repo 63 | req.Repos = append(req.GetRepos(), &data.Repo{ 64 | Url: proto.String(localRepo), 65 | Commit: proto.String(headSHA), 66 | Metadata: []string{}, 67 | }) 68 | } 69 | return req 70 | } 71 | 72 | func resultExists(ctx context.Context, sbr *data.ScorecardBatchRequest, bucketURL string) (bool, error) { 73 | exists, err := data.BlobExists(ctx, bucketURL, worker.ResultFilename(sbr)) 74 | if err != nil { 75 | return false, fmt.Errorf("blob exists check: %w", err) 76 | } 77 | return exists, nil 78 | } 79 | -------------------------------------------------------------------------------- /cmd/collect_signals/vcs/vcs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package vcs 16 | 17 | import ( 18 | "runtime/debug" 19 | ) 20 | 21 | const ( 22 | MissingCommitID = "-missing-" 23 | commitIDKey = "vcs.revision" 24 | ) 25 | 26 | var commitID string 27 | 28 | func fetchCommitID() string { 29 | info, ok := debug.ReadBuildInfo() 30 | if !ok { 31 | return MissingCommitID 32 | } 33 | 34 | for _, setting := range info.Settings { 35 | if setting.Key == commitIDKey { 36 | return setting.Value 37 | } 38 | } 39 | 40 | return MissingCommitID 41 | } 42 | 43 | // CommitID returns the vcs commit ID embedded in the binary when the 44 | // -buildvcs flag is set while building. 45 | func CommitID() string { 46 | if commitID == "" { 47 | commitID = fetchCommitID() 48 | } 49 | return commitID 50 | } 51 | -------------------------------------------------------------------------------- /cmd/criticality_score/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM golang:1.24.2@sha256:1ecc479bc712a6bdb56df3e346e33edcc141f469f82840bab9f4bc2bc41bf91d AS base 16 | WORKDIR /src 17 | ENV CGO_ENABLED=0 18 | COPY go.mod go.sum ./ 19 | RUN go mod download 20 | COPY . ./ 21 | 22 | FROM base AS criticality_score 23 | ARG TARGETOS 24 | ARG TARGETARCH 25 | RUN CGO_ENABLED=0 go build ./cmd/criticality_score 26 | RUN chmod -R 0775 /src/config/scorer/* 27 | 28 | FROM gcr.io/distroless/base:nonroot@sha256:fa5f94fa433728f8df3f63363ffc8dec4adcfb57e4d8c18b44bceccfea095ebc 29 | COPY --from=criticality_score /src/criticality_score ./criticality_score 30 | COPY --from=criticality_score /src/config/scorer/* ./config/scorer/ 31 | ENTRYPOINT ["./criticality_score"] 32 | -------------------------------------------------------------------------------- /cmd/criticality_score/inputiter/err_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build windows 16 | 17 | package inputiter 18 | 19 | import ( 20 | "errors" 21 | 22 | "golang.org/x/sys/windows" 23 | ) 24 | 25 | func init() { 26 | // Windows can return an additional error number when there is a failure. 27 | osErrorWithFilename = func(err error) bool { 28 | return errors.Is(err, windows.ERROR_INVALID_NAME) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cmd/criticality_score/inputiter/new.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package inputiter 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "net/url" 21 | "os" 22 | 23 | "github.com/ossf/criticality_score/v2/internal/infile" 24 | "github.com/ossf/criticality_score/v2/internal/iterator" 25 | ) 26 | 27 | // osErrorWithFilename is an os-specific helper for determining if a particular 28 | // error is related to the filename of the file. 29 | var osErrorWithFilename func(err error) bool 30 | 31 | // errWithFilename determines if the given error is the result of an error 32 | // caused by the filename being invalid, or pointing to a filename that doesn't 33 | // exist. 34 | func errWithFilename(err error) bool { 35 | switch { 36 | case errors.Is(err, os.ErrNotExist): 37 | return true 38 | case osErrorWithFilename != nil && osErrorWithFilename(err): 39 | return true 40 | default: 41 | return false 42 | } 43 | } 44 | 45 | // Iterator returns an IterCloser for iterating across repositories for 46 | // collecting signals. 47 | // 48 | // If only one arg is specified, the code will treat it as a file and attempt to 49 | // open it. If the file doesn't exist, and is parseable as a URL the arg will be 50 | // treated as a repo. 51 | // 52 | // If more than one arg is specified they are all considered to be repos. 53 | // 54 | // TODO: support the ability to force args to be interpreted as either a file, 55 | // or a list of repos. 56 | func New(args []string) (iterator.IterCloser[string], error) { 57 | if len(args) == 1 { 58 | // If there is 1 arg, attempt to open it as a file. 59 | fileOrRepo := args[0] 60 | urlParseFailed := false 61 | if _, err := url.Parse(fileOrRepo); err != nil { 62 | urlParseFailed = true 63 | } 64 | 65 | // Open the in-file for reading 66 | r, err := infile.Open(context.Background(), fileOrRepo) 67 | if err == nil { 68 | return iterator.Lines(r), nil 69 | } 70 | if urlParseFailed || !errWithFilename(err) { 71 | // Only report errors if the file doesn't appear to be a URL, if the 72 | // filename doesn't exist, or the filename is invalid. 73 | return nil, err 74 | } 75 | } 76 | // If file loading failed, or there are 2 or more args, treat args as a list 77 | // of repos. 78 | return iterator.Slice(args), nil 79 | } 80 | -------------------------------------------------------------------------------- /cmd/criticality_score/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | // These variables are populated by GoReleaser. They can be set manually on the 6 | // command line using -ldflags. For example: 7 | // 8 | // go build -ldflags="-X 'main.version=x' -X 'main.date=y' -X 'main.commit=z'" ./cmd/criticality_score 9 | var ( 10 | version = "dev" 11 | commit = "none" 12 | date = "unknown" 13 | ) 14 | 15 | func printVersion() { 16 | if version == "dev" { 17 | fmt.Printf("dev build") 18 | } else { 19 | fmt.Printf("v%s (%s - %s)\n", version, date, commit) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cmd/csv_transfer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM golang:1.24.2@sha256:1ecc479bc712a6bdb56df3e346e33edcc141f469f82840bab9f4bc2bc41bf91d AS base 16 | WORKDIR /src 17 | ENV CGO_ENABLED=0 18 | COPY go.mod go.sum ./ 19 | RUN go mod download 20 | COPY . ./ 21 | 22 | FROM base AS csv_transfer 23 | ARG TARGETOS 24 | ARG TARGETARCH 25 | RUN CGO_ENABLED=0 go build ./cmd/csv_transfer 26 | 27 | FROM gcr.io/distroless/base:nonroot@sha256:fa5f94fa433728f8df3f63363ffc8dec4adcfb57e4d8c18b44bceccfea095ebc 28 | COPY --from=csv_transfer /src/csv_transfer ./csv_transfer 29 | ENTRYPOINT ["./csv_transfer"] 30 | -------------------------------------------------------------------------------- /cmd/csv_transfer/README.md: -------------------------------------------------------------------------------- 1 | # CSV Transfer 2 | 3 | This tool runs across the CSV output for each shard produced by 4 | `collect_signals` and aggregates the output into a single CSV file. 5 | -------------------------------------------------------------------------------- /cmd/enumerate_github/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM golang:1.24.2@sha256:1ecc479bc712a6bdb56df3e346e33edcc141f469f82840bab9f4bc2bc41bf91d AS base 16 | WORKDIR /src 17 | ENV CGO_ENABLED=0 18 | COPY go.mod go.sum ./ 19 | RUN go mod download 20 | COPY . ./ 21 | 22 | FROM base AS enumerate_github 23 | ARG TARGETOS 24 | ARG TARGETARCH 25 | RUN CGO_ENABLED=0 go build ./cmd/enumerate_github 26 | 27 | FROM gcr.io/distroless/base:nonroot@sha256:fa5f94fa433728f8df3f63363ffc8dec4adcfb57e4d8c18b44bceccfea095ebc 28 | COPY --from=enumerate_github /src/enumerate_github ./enumerate_github 29 | ENTRYPOINT ["./enumerate_github"] 30 | -------------------------------------------------------------------------------- /cmd/enumerate_github/githubsearch/search.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package githubsearch 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | 21 | "github.com/hasura/go-graphql-client" 22 | 23 | "go.uber.org/zap" 24 | ) 25 | 26 | // empty is a convenience wrapper for the empty struct. 27 | type empty struct{} 28 | 29 | var ErrorUnableToListAllResult = errors.New("unable to list all results") 30 | 31 | type Searcher struct { 32 | ctx context.Context 33 | client *graphql.Client 34 | logger *zap.Logger 35 | perPage int 36 | } 37 | 38 | type Option interface { 39 | set(*Searcher) 40 | } 41 | 42 | // option implements Option. 43 | type option func(*Searcher) 44 | 45 | func (o option) set(s *Searcher) { o(s) } 46 | 47 | // PerPage will set how many results will per requested per page for each search query. 48 | func PerPage(perPage int) Option { 49 | return option(func(s *Searcher) { s.perPage = perPage }) 50 | } 51 | 52 | func NewSearcher(ctx context.Context, client *graphql.Client, logger *zap.Logger, options ...Option) *Searcher { 53 | s := &Searcher{ 54 | ctx: ctx, 55 | client: client, 56 | logger: logger, 57 | perPage: 100, 58 | } 59 | for _, o := range options { 60 | o.set(s) 61 | } 62 | return s 63 | } 64 | -------------------------------------------------------------------------------- /cmd/enumerate_github/marker/type.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package marker 16 | 17 | import ( 18 | "bytes" 19 | "errors" 20 | "net/url" 21 | "path" 22 | "strings" 23 | ) 24 | 25 | type Type int 26 | 27 | const ( 28 | TypeFull = Type(iota) 29 | TypeFile 30 | TypeDir 31 | ) 32 | 33 | var ErrorUnknownType = errors.New("unknown marker type") 34 | 35 | // String implements the fmt.Stringer interface. 36 | func (t Type) String() string { 37 | text, err := t.MarshalText() 38 | if err != nil { 39 | return "" 40 | } 41 | return string(text) 42 | } 43 | 44 | // MarshalText implements the encoding.TextMarshaler interface. 45 | func (t Type) MarshalText() ([]byte, error) { 46 | switch t { 47 | case TypeFull: 48 | return []byte("full"), nil 49 | case TypeFile: 50 | return []byte("file"), nil 51 | case TypeDir: 52 | return []byte("dir"), nil 53 | default: 54 | return []byte{}, ErrorUnknownType 55 | } 56 | } 57 | 58 | // UnmarshalText implements the encoding.TextUnmarshaler interface. 59 | func (t *Type) UnmarshalText(text []byte) error { 60 | switch { 61 | case bytes.Equal(text, []byte("full")): 62 | *t = TypeFull 63 | case bytes.Equal(text, []byte("file")): 64 | *t = TypeFile 65 | case bytes.Equal(text, []byte("dir")): 66 | *t = TypeDir 67 | default: 68 | return ErrorUnknownType 69 | } 70 | return nil 71 | } 72 | 73 | func (t Type) transform(p string) string { 74 | if t == TypeFull { 75 | return p 76 | } 77 | // Is this a bucket URL? 78 | if u, err := url.Parse(p); err == nil && u.IsAbs() { 79 | if u.Scheme == "file" && u.Host == "" { 80 | p = u.Path 81 | } else { 82 | p = strings.TrimPrefix(u.Path, "/") 83 | } 84 | } 85 | if t == TypeDir { 86 | return path.Dir(p) 87 | } 88 | return p 89 | } 90 | -------------------------------------------------------------------------------- /cmd/enumerate_github/marker/write.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package marker 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/ossf/criticality_score/v2/internal/cloudstorage" 22 | ) 23 | 24 | func Write(ctx context.Context, t Type, markerFile, outFile string) (err error) { 25 | marker, e := cloudstorage.NewWriter(ctx, markerFile) 26 | if e != nil { 27 | return fmt.Errorf("open marker: %w", e) 28 | } 29 | defer func() { 30 | if e := marker.Close(); e != nil { 31 | // Return the error using the named return value if it isn't 32 | // already set. 33 | if e == nil { 34 | err = fmt.Errorf("closing marker: %w", e) 35 | } 36 | } 37 | }() 38 | if _, e := fmt.Fprintln(marker, t.transform(outFile)); e != nil { 39 | err = fmt.Errorf("writing marker: %w", e) 40 | } 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /cmd/enumerate_github/marker/write_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build !windows 16 | 17 | package marker_test 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "path" 23 | "strings" 24 | "testing" 25 | 26 | "github.com/ossf/criticality_score/v2/cmd/enumerate_github/marker" 27 | ) 28 | 29 | func TestWrite(t *testing.T) { 30 | want := "this/is/a/path" 31 | dir := t.TempDir() 32 | file := path.Join(dir, "marker.test") 33 | 34 | err := marker.Write(context.Background(), marker.TypeFull, file, want) 35 | if err != nil { 36 | t.Fatalf("Write() = %v, want no error", err) 37 | } 38 | 39 | markerContents, err := os.ReadFile(file) 40 | if err != nil { 41 | t.Fatalf("ReadFile() = %v, want no error", err) 42 | } 43 | if got := strings.TrimSpace(string(markerContents)); got != want { 44 | t.Fatalf("marker contents = %q, want %q", got, want) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cmd/enumerate_github/repowriter/scorecard.go: -------------------------------------------------------------------------------- 1 | package repowriter 2 | 3 | import ( 4 | "encoding/csv" 5 | "io" 6 | ) 7 | 8 | var header = []string{"repo", "metadata"} 9 | 10 | type scorecardWriter struct { 11 | w *csv.Writer 12 | } 13 | 14 | // Scorecard creates a new Writer instance that is used to write a csv file 15 | // of repositories that is compatible with the github.com/ossf/scorecard 16 | // project. 17 | // 18 | // The csv file has a header row with columns "repo" and "metadata". Each 19 | // row consists of the repository url and blank metadata. 20 | func Scorecard(w io.Writer) Writer { 21 | csvWriter := csv.NewWriter(w) 22 | csvWriter.Write(header) 23 | return &scorecardWriter{w: csvWriter} 24 | } 25 | 26 | // Write implements the Writer interface. 27 | func (w *scorecardWriter) Write(repo string) error { 28 | if err := w.w.Write([]string{repo, ""}); err != nil { 29 | return err 30 | } 31 | w.w.Flush() 32 | return w.w.Error() 33 | } 34 | -------------------------------------------------------------------------------- /cmd/enumerate_github/repowriter/scorecard_test.go: -------------------------------------------------------------------------------- 1 | package repowriter_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | 9 | "github.com/ossf/criticality_score/v2/cmd/enumerate_github/repowriter" 10 | ) 11 | 12 | func TestScorecardRepoWriter(t *testing.T) { 13 | var buf bytes.Buffer 14 | w := repowriter.Scorecard(&buf) 15 | w.Write("https://github.com/example/example") 16 | w.Write("https://github.com/ossf/criticality_score") 17 | 18 | want := "repo,metadata\n" + 19 | "https://github.com/example/example,\n" + 20 | "https://github.com/ossf/criticality_score,\n" 21 | 22 | if diff := cmp.Diff(want, buf.String()); diff != "" { 23 | t.Fatalf("Scorecard() mismatch (-want +got):\n%s", diff) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cmd/enumerate_github/repowriter/text.go: -------------------------------------------------------------------------------- 1 | package repowriter 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | type textWriter struct { 9 | w io.Writer 10 | } 11 | 12 | // Text creates a new Writer instance that is used to write a simple text file 13 | // of repositories, where each line has a single repository url. 14 | func Text(w io.Writer) Writer { 15 | return &textWriter{w} 16 | } 17 | 18 | // Write implements the Writer interface. 19 | func (w *textWriter) Write(repo string) error { 20 | _, err := fmt.Fprintln(w.w, repo) 21 | return err 22 | } 23 | -------------------------------------------------------------------------------- /cmd/enumerate_github/repowriter/text_test.go: -------------------------------------------------------------------------------- 1 | package repowriter_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | 9 | "github.com/ossf/criticality_score/v2/cmd/enumerate_github/repowriter" 10 | ) 11 | 12 | func TestTextRepoWriter(t *testing.T) { 13 | var buf bytes.Buffer 14 | w := repowriter.Text(&buf) 15 | w.Write("https://github.com/example/example") 16 | w.Write("https://github.com/ossf/criticality_score") 17 | 18 | want := "https://github.com/example/example\n" + 19 | "https://github.com/ossf/criticality_score\n" 20 | 21 | if diff := cmp.Diff(want, buf.String()); diff != "" { 22 | t.Fatalf("Text() mismatch (-want +got):\n%s", diff) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cmd/enumerate_github/repowriter/type.go: -------------------------------------------------------------------------------- 1 | package repowriter 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | ) 8 | 9 | type WriterType int 10 | 11 | const ( 12 | // WriterTypeText corresponds to the Writer returned by Text. 13 | WriterTypeText = WriterType(iota) 14 | 15 | // WriterTypeScorecard corresponds to the Writer returned by Scorecard. 16 | WriterTypeScorecard 17 | ) 18 | 19 | var ErrorUnknownRepoWriterType = errors.New("unknown repo writer type") 20 | 21 | // String implements the fmt.Stringer interface. 22 | func (t WriterType) String() string { 23 | text, err := t.MarshalText() 24 | if err != nil { 25 | return "" 26 | } 27 | return string(text) 28 | } 29 | 30 | // MarshalText implements the encoding.TextMarshaler interface. 31 | func (t WriterType) MarshalText() ([]byte, error) { 32 | switch t { 33 | case WriterTypeText: 34 | return []byte("text"), nil 35 | case WriterTypeScorecard: 36 | return []byte("scorecard"), nil 37 | default: 38 | return []byte{}, ErrorUnknownRepoWriterType 39 | } 40 | } 41 | 42 | // UnmarshalText implements the encoding.TextUnmarshaler interface. 43 | func (t *WriterType) UnmarshalText(text []byte) error { 44 | switch { 45 | case bytes.Equal(text, []byte("text")): 46 | *t = WriterTypeText 47 | case bytes.Equal(text, []byte("scorecard")): 48 | *t = WriterTypeScorecard 49 | default: 50 | return ErrorUnknownRepoWriterType 51 | } 52 | return nil 53 | } 54 | 55 | // New will return a new instance of the corresponding implementation of 56 | // Writer for the given WriterType. 57 | func (t *WriterType) New(w io.Writer) Writer { 58 | switch *t { 59 | case WriterTypeText: 60 | return Text(w) 61 | case WriterTypeScorecard: 62 | return Scorecard(w) 63 | default: 64 | return nil 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cmd/enumerate_github/repowriter/type_test.go: -------------------------------------------------------------------------------- 1 | package repowriter_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/ossf/criticality_score/v2/cmd/enumerate_github/repowriter" 8 | ) 9 | 10 | func TestTypeString(t *testing.T) { 11 | //nolint:govet 12 | tests := []struct { 13 | name string 14 | writerType repowriter.WriterType 15 | want string 16 | }{ 17 | {name: "text", writerType: repowriter.WriterTypeText, want: "text"}, 18 | {name: "scorecard", writerType: repowriter.WriterTypeScorecard, want: "scorecard"}, 19 | {name: "unknown", writerType: repowriter.WriterType(10), want: ""}, 20 | } 21 | for _, test := range tests { 22 | t.Run(test.name, func(t *testing.T) { 23 | got := test.writerType.String() 24 | if got != test.want { 25 | t.Fatalf("String() == %s, want %s", got, test.want) 26 | } 27 | }) 28 | } 29 | } 30 | 31 | func TestTypeMarshalText(t *testing.T) { 32 | //nolint:govet 33 | tests := []struct { 34 | name string 35 | writerType repowriter.WriterType 36 | want string 37 | err error 38 | }{ 39 | {name: "text", writerType: repowriter.WriterTypeText, want: "text"}, 40 | {name: "scorecard", writerType: repowriter.WriterTypeScorecard, want: "scorecard"}, 41 | {name: "unknown", writerType: repowriter.WriterType(10), want: "", err: repowriter.ErrorUnknownRepoWriterType}, 42 | } 43 | for _, test := range tests { 44 | t.Run(test.name, func(t *testing.T) { 45 | got, err := test.writerType.MarshalText() 46 | if err != nil && !errors.Is(err, test.err) { 47 | t.Fatalf("MarhsalText() == %v, want %v", err, test.err) 48 | } 49 | if err == nil { 50 | if test.err != nil { 51 | t.Fatalf("MarshalText() return nil error, want %v", test.err) 52 | } 53 | if string(got) != test.want { 54 | t.Fatalf("MarhsalText() == %s, want %s", got, test.want) 55 | } 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestTypeUnmarshalText(t *testing.T) { 62 | //nolint:govet 63 | tests := []struct { 64 | input string 65 | want repowriter.WriterType 66 | err error 67 | }{ 68 | {input: "text", want: repowriter.WriterTypeText}, 69 | {input: "scorecard", want: repowriter.WriterTypeScorecard}, 70 | {input: "", want: 0, err: repowriter.ErrorUnknownRepoWriterType}, 71 | {input: "unknown", want: 0, err: repowriter.ErrorUnknownRepoWriterType}, 72 | } 73 | for _, test := range tests { 74 | t.Run(test.input, func(t *testing.T) { 75 | var got repowriter.WriterType 76 | err := got.UnmarshalText([]byte(test.input)) 77 | if err != nil && !errors.Is(err, test.err) { 78 | t.Fatalf("UnmarshalText() == %v, want %v", err, test.err) 79 | } 80 | if err == nil { 81 | if test.err != nil { 82 | t.Fatalf("MarshalText() return nil error, want %v", test.err) 83 | } 84 | if got != test.want { 85 | t.Fatalf("UnmarshalText() parsed %d, want %d", int(got), int(test.want)) 86 | } 87 | } 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cmd/enumerate_github/repowriter/writer.go: -------------------------------------------------------------------------------- 1 | package repowriter 2 | 3 | // Writer is a simple interface for writing a repo. This interface is to 4 | // abstract output formats for lists of repository urls. 5 | type Writer interface { 6 | // Write outputs a single repository url. 7 | Write(repo string) error 8 | } 9 | -------------------------------------------------------------------------------- /cmd/scorer/pq.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import "container/heap" 18 | 19 | // A RowItem is something to manage in a priority queue. 20 | type RowItem struct { 21 | row []string 22 | score float64 23 | // The index is needed by update and is maintained by the heap.Interface methods. 24 | index int // The index of the item in the heap. 25 | } 26 | 27 | // A PriorityQueue implements heap.Interface and holds RowItems. 28 | // 29 | // The implementation of PriorityQueue is largely copied from the 30 | // "container/heap" documentation. 31 | type PriorityQueue []*RowItem 32 | 33 | // Len implements the heap.Interface interface. 34 | func (pq PriorityQueue) Len() int { return len(pq) } 35 | 36 | // Less implements the heap.Interface interface. 37 | func (pq PriorityQueue) Less(i, j int) bool { 38 | // We want Pop to give us the highest, not lowest, priority so we use greater than here. 39 | return pq[i].score > pq[j].score 40 | } 41 | 42 | // Swap implements the heap.Interface interface. 43 | func (pq PriorityQueue) Swap(i, j int) { 44 | pq[i], pq[j] = pq[j], pq[i] 45 | pq[i].index = i 46 | pq[j].index = j 47 | } 48 | 49 | // Push implements the heap.Interface interface. 50 | func (pq *PriorityQueue) Push(x any) { 51 | n := len(*pq) 52 | item := x.(*RowItem) 53 | item.index = n 54 | *pq = append(*pq, item) 55 | } 56 | 57 | // Pop implements the heap.Interface interface. 58 | func (pq *PriorityQueue) Pop() any { 59 | old := *pq 60 | n := len(old) 61 | item := old[n-1] 62 | old[n-1] = nil // avoid memory leak 63 | item.index = -1 // for safety 64 | *pq = old[0 : n-1] 65 | return item 66 | } 67 | 68 | // PushRow will add the given row into the priority queue with the score as the 69 | // priority. 70 | func (pq *PriorityQueue) PushRow(row []string, score float64) { 71 | heap.Push(pq, &RowItem{ 72 | row: row, 73 | score: score, 74 | }) 75 | } 76 | 77 | // PopRow returns the row with the highest score. 78 | func (pq *PriorityQueue) PopRow() []string { 79 | return heap.Pop(pq).(*RowItem).row 80 | } 81 | -------------------------------------------------------------------------------- /config/scorer/original_pike.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # The original set of weights defined for the criticality score project using 16 | # Rob Pike's algorithm as implemented in the original Python project. 17 | algorithm: weighted_arithmetic_mean 18 | 19 | inputs: 20 | - field: legacy.created_since 21 | weight: 1 22 | bounds: 23 | upper: 120 24 | distribution: zipfian 25 | 26 | - field: legacy.updated_since 27 | weight: 1 28 | bounds: 29 | upper: 120 30 | smaller_is_better: yes 31 | distribution: zipfian 32 | 33 | - field: legacy.contributor_count 34 | weight: 2 35 | bounds: 36 | upper: 5000 37 | distribution: zipfian 38 | 39 | - field: legacy.org_count 40 | weight: 1 41 | bounds: 42 | upper: 10 43 | distribution: zipfian 44 | 45 | - field: legacy.commit_frequency 46 | weight: 1 47 | bounds: 48 | upper: 1000 49 | distribution: zipfian 50 | 51 | - field: legacy.recent_release_count 52 | weight: 0.5 53 | bounds: 54 | upper: 26 55 | distribution: zipfian 56 | 57 | - field: legacy.updated_issues_count 58 | weight: 0.5 59 | bounds: 60 | upper: 5000 61 | distribution: zipfian 62 | 63 | - field: legacy.closed_issues_count 64 | weight: 0.5 65 | bounds: 66 | upper: 5000 67 | distribution: zipfian 68 | 69 | - field: legacy.issue_comment_frequency 70 | weight: 1 71 | bounds: 72 | upper: 15 73 | distribution: zipfian 74 | 75 | - field: legacy.github_mention_count 76 | weight: 2 77 | bounds: 78 | upper: 500000 79 | distribution: zipfian 80 | -------------------------------------------------------------------------------- /config/scorer/pike_depsdev.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | algorithm: weighted_arithmetic_mean 16 | 17 | inputs: 18 | - field: legacy.created_since 19 | weight: 1 20 | bounds: 21 | upper: 120 22 | distribution: zipfian 23 | 24 | - field: legacy.updated_since 25 | weight: 1 26 | bounds: 27 | upper: 120 28 | smaller_is_better: yes 29 | distribution: zipfian 30 | 31 | - field: legacy.contributor_count 32 | weight: 2 33 | bounds: 34 | upper: 5000 35 | distribution: zipfian 36 | 37 | - field: legacy.org_count 38 | weight: 1 39 | bounds: 40 | upper: 10 41 | distribution: zipfian 42 | 43 | - field: legacy.commit_frequency 44 | weight: 1 45 | bounds: 46 | upper: 1000 47 | distribution: zipfian 48 | 49 | - field: legacy.recent_release_count 50 | weight: 0.5 51 | bounds: 52 | upper: 26 53 | distribution: zipfian 54 | 55 | - field: legacy.updated_issues_count 56 | weight: 0.5 57 | bounds: 58 | upper: 5000 59 | distribution: zipfian 60 | 61 | - field: legacy.closed_issues_count 62 | weight: 0.5 63 | bounds: 64 | upper: 5000 65 | distribution: zipfian 66 | 67 | - field: legacy.issue_comment_frequency 68 | weight: 1 69 | bounds: 70 | upper: 15 71 | distribution: zipfian 72 | 73 | # If deps.dev dependenct count doesn't exist we use this configuration 74 | # for the GitHub search mention count. 75 | - field: legacy.github_mention_count 76 | weight: 2 77 | bounds: 78 | upper: 500000 79 | condition: 80 | not: 81 | field_exists: "depsdev.dependent_count" 82 | distribution: zipfian 83 | 84 | # If deps.dev dependent count *does* exist we lower the importance of 85 | # the GitHub search mention count. 86 | - field: legacy.github_mention_count 87 | weight: 1 88 | bounds: 89 | upper: 500000 90 | condition: 91 | field_exists: "depsdev.dependent_count" 92 | distribution: zipfian 93 | 94 | - field: depsdev.dependent_count 95 | weight: 4 96 | bounds: 97 | upper: 200000 98 | distribution: zipfian 99 | -------------------------------------------------------------------------------- /docs/glossary.md: -------------------------------------------------------------------------------- 1 | # Glossary 2 | 3 | This document defines the meaning of various terms used by this project. 4 | This is to ensure they are clearly understood. 5 | 6 | Please keep the document sorted alphabetically. 7 | 8 | ## Terms 9 | 10 | ### Fork 11 | 12 | A _fork_, like a mirror, is a clone or copy of another project's source code or 13 | repository. 14 | 15 | A fork has two primary uses: 16 | 17 | * A contributor commiting changes to a fork for preparing pull-requests to the 18 | main repository. 19 | * A fork may become its own project when the original is unmaintained, or if 20 | the forker decides to head in a different direction. 21 | 22 | Forks merely used for committing changes for a pull-request are not interesting 23 | when calculating criticality scores. 24 | 25 | See also "Mirror". 26 | 27 | ### Mirror 28 | 29 | A _mirror_, like a fork, is a clone or copy of another project's source code or 30 | repository. 31 | 32 | A mirror is usually used to provide broader access to a repository, such as 33 | when a self-hosted project mirrors its repository on GitHub. 34 | 35 | Mirrors may require de-duping to avoid treating the original repository and 36 | its mirrors as separate projects. 37 | 38 | See also "Fork". 39 | 40 | ### Project 41 | 42 | A _project_ is defined as only having a _single repository_, and a _single 43 | issue tracker_. A project may provide multiple _packages_. 44 | 45 | There are some "umbrella projects" (e.g. Kubernetes) that have multiple 46 | repositories associated with them, or may use a centralized issue tracker. An 47 | alternative approach would be to treat a project independently of the one or 48 | more repositories that belong to it. 49 | 50 | However this approach has the following drawbacks: 51 | 52 | * Makes it hard to distinguish between organizations and umbrella projects 53 | * Raises the possibility that a part of the umbrella project that is critical 54 | to OSS is missed. 55 | * Complicates the calculation required to aggregate signals and generate a 56 | criticality score. 57 | 58 | So instead we define a project as a single repository. This provides a clear 59 | "primary key" we can use for collecting signals. 60 | 61 | ### Repository 62 | 63 | A _repository_ refers to the system used to store and manage access to a 64 | project's source code. Usually a version control system (e.g. git or mercurial) 65 | is used to track and manage changes to the source code. 66 | 67 | A _repository_ can be the canonical source of a project's code, or it could 68 | also be a _fork_ or a _mirror_. 69 | 70 | A _repository_ is usually owned by an individual or an organization, although 71 | the specifics of how this behaves in practice depends on the repositories host. 72 | 73 | ### Repository Host 74 | 75 | A _repository host_ is the service hosting a _repository_. It may be a service 76 | such as GitHub, GitLab or Bitbucket. It may also be "self-hosted", where the 77 | infrastructure for hosting a repository is managed by the maintainers of a 78 | project. 79 | 80 | Self-hosted repositories often deploy an open-source application to provide 81 | access, such as GitLab, cGit, or Gitea. 82 | 83 | ### Umbrella Project 84 | 85 | An _umbrella project_ is a group of related projects that are maintained by a 86 | larger community surrounding the project. 87 | 88 | See also "project". -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.24.2 2 | 3 | use ( 4 | . 5 | ./tools 6 | ) 7 | -------------------------------------------------------------------------------- /images/formula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ossf/criticality_score/47238f446aac2e6ce83b2597c8feb58f156a9bb1/images/formula.png -------------------------------------------------------------------------------- /infra/README.md: -------------------------------------------------------------------------------- 1 | # Infrastructure 2 | 3 | This directory contains various infrastructure related components. 4 | 5 | ## Contents 6 | 7 | - **cloudbuild**: various Google Cloud Build configurations for automated 8 | building container images and kicking off new releases in Google Cloud Deploy. 9 | - **envs**: Kubernetes and Kustomize based configurations for staging and prod 10 | environments. 11 | - **base**: shared configuration for prod and staging 12 | - **prod**: production specific configuration 13 | - **staging**: staging specific configuration 14 | - **k8s**: Misc Kubernetes configurations for Pods that live outside the staging 15 | and prod environments. 16 | - **test**: A Docker Compose version of the infrastructure for local testing and 17 | validation. 18 | - **clouddeploy.yaml**, **skaffold.yaml**: Google Cloud Deploy configuration for 19 | managing the release process of the Criticality Score infrastructure. 20 | 21 | ## Deploy Process 22 | 23 | 1. When a new commit is made to `main` (e.g. merged PR): 24 | - `collect-signals` container image is built and pushed. 25 | - `csv-transfer` container image is built and pushed. 26 | - `enumerate-github` container image is built and pushed. 27 | 1. Every weekday a scheduled trigger starts a Google Cloud Build process (see 28 | [release.yaml](https://github.com/ossf/criticality_score/blob/main/infra/cloudbuild/release.yaml)). 29 | 1. `collect-signals` and `csv-transfer` container images are pulled for the 30 | current commit SHA, ensuring the container images are present. 31 | 1. A new Cloud Deploy release is created (if it doesn't exist already). 32 | - Release named: `rel-${SHORT_SHA}`. 33 | - Images are tagged with `${$COMMIT_SHA}` and used in the release. 34 | - Scorecard images are hardcoded to match go.mod. 35 | 1. Cloud Deploy automatically releases to the 36 | [staging environment](https://github.com/ossf/criticality_score/blob/main/infra/envs/staging). 37 | - The staging environment runs a short run each weekday. 38 | 1. Once a staging release is confirmed as working, the release can be promoted 39 | to the [production environment](https://github.com/ossf/criticality_score/blob/main/infra/envs/prod). 40 | - Ideally this should be done between executions to avoid version skew 41 | issues. 42 | 43 | ## Cheat Sheet 44 | 45 | ### Skaffold and Kustomize Output Inspection 46 | 47 | To inspect the expanded Kubernetes configuration for each environment use the 48 | following commands, replacing the environment with the relevant one. 49 | 50 | For Kustomize (fast): 51 | 52 | ```shell 53 | kubectl kustomize ./infra/envs/{staging,prod} 54 | ``` 55 | 56 | For Skaffold : 57 | 58 | ```shell 59 | cd infra && \ 60 | skaffold render -f ./skaffold.yaml --offline -p {staging,prod} 61 | ``` 62 | 63 | ### Kubernetes Information 64 | 65 | Connecting to the cluster with `gcloud` 66 | 67 | ```shell 68 | gcloud container clusters get-credentials --region us-central1-c criticality-score 69 | ``` 70 | 71 | Verify context: 72 | 73 | ```shell 74 | kubectl config current-context 75 | ``` 76 | 77 | #### Managing Secrets 78 | 79 | Updating Production GitHub access tokens: 80 | 81 | ```shell 82 | kubectl create secret generic github --from-literal=token=$GITHUB_AUTH_TOKENS 83 | ``` 84 | 85 | Updating Staging GitHub access tokens: 86 | 87 | ```shell 88 | kubectl create secret generic github-staging --from-literal=token=$GITHUB_AUTH_TOKENS 89 | ``` 90 | 91 | **Note:** `github` and `github-staging` must be disjoint sets of GitHub 92 | personal access tokens. If they share tokens one environment may starve the 93 | other. 94 | -------------------------------------------------------------------------------- /infra/cloudbuild/collect_signals/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | steps: 16 | - name: 'gcr.io/cloud-builders/docker' 17 | args: ['build', '.', 18 | '--build-arg', 'COMMIT_SHA=$COMMIT_SHA', 19 | '-t', 'gcr.io/openssf/criticality-score-collect-signals:$COMMIT_SHA', 20 | '-t', 'gcr.io/openssf/criticality-score-collect-signals:latest', 21 | '-f', 'cmd/collect_signals/Dockerfile'] 22 | images: ['gcr.io/openssf/criticality-score-collect-signals'] 23 | -------------------------------------------------------------------------------- /infra/cloudbuild/csv_transfer/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | steps: 16 | - name: 'gcr.io/cloud-builders/docker' 17 | args: ['build', '.', 18 | '--build-arg', 'COMMIT_SHA=$COMMIT_SHA', 19 | '-t', 'gcr.io/openssf/criticality-score-csv-transfer:$COMMIT_SHA', 20 | '-t', 'gcr.io/openssf/criticality-score-csv-transfer:latest', 21 | '-f', 'cmd/csv_transfer/Dockerfile'] 22 | images: ['gcr.io/openssf/criticality-score-csv-transfer'] 23 | -------------------------------------------------------------------------------- /infra/cloudbuild/enumerate_github/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | steps: 16 | - name: 'gcr.io/cloud-builders/docker' 17 | args: ['build', '.', 18 | '--build-arg', 'COMMIT_SHA=$COMMIT_SHA', 19 | '-t', 'gcr.io/openssf/criticality-score-enumerate-github:$COMMIT_SHA', 20 | '-t', 'gcr.io/openssf/criticality-score-enumerate-github:latest', 21 | '-f', 'cmd/enumerate_github/Dockerfile'] 22 | images: ['gcr.io/openssf/criticality-score-enumerate-github'] 23 | -------------------------------------------------------------------------------- /infra/cloudbuild/init_collect_signals/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | steps: 16 | - name: 'gcr.io/cloud-builders/docker' 17 | args: ['build', '.', 18 | '--build-arg', 'COMMIT_SHA=$COMMIT_SHA', 19 | '-t', 'gcr.io/openssf/criticality-score-init-collect-signals:$COMMIT_SHA', 20 | '-t', 'gcr.io/openssf/criticality-score-init-collect-signals:latest', 21 | '-f', 'infra/images/init_collect_signals/Dockerfile'] 22 | images: ['gcr.io/openssf/criticality-score-init-collect-signals'] 23 | -------------------------------------------------------------------------------- /infra/cloudbuild/release.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | steps: 16 | 17 | # Ensure criticality-score-collect-signals docker image already exists. 18 | - name: 'gcr.io/cloud-builders/docker' 19 | args: ['pull', 'gcr.io/openssf/criticality-score-collect-signals:$COMMIT_SHA'] 20 | 21 | # Ensure criticality-score-init-collect-signals docker image already exists. 22 | - name: 'gcr.io/cloud-builders/docker' 23 | args: ['pull', 'gcr.io/openssf/criticality-score-init-collect-signals:$COMMIT_SHA'] 24 | 25 | # Ensure criticality-score-csv-transfer docker image already exists. 26 | - name: 'gcr.io/cloud-builders/docker' 27 | args: ['pull', 'gcr.io/openssf/criticality-score-csv-transfer:$COMMIT_SHA'] 28 | 29 | # Start a Cloud Deploy replease. Ignore failures as this will get run daily, 30 | # even if latest commit hasn't changed. 31 | - name: gcr.io/google.com/cloudsdktool/cloud-sdk 32 | allowFailure: true 33 | entrypoint: gcloud 34 | args: 35 | [ 36 | "deploy", "releases", "create", "rel-${SHORT_SHA}", 37 | "--delivery-pipeline", "criticality-score-deploy", 38 | "--region", "us-central1", 39 | "--annotations", "commitId=${REVISION_ID}", 40 | "--skaffold-file=./infra/skaffold.yaml", 41 | "--images", "collect-signals=gcr.io/openssf/criticality-score-collect-signals:$COMMIT_SHA,init-collect-signals=gcr.io/openssf/criticality-score-init-collect-signals:$COMMIT_SHA,csv-transfer=gcr.io/openssf/criticality-score-csv-transfer:$COMMIT_SHA" 42 | ] 43 | -------------------------------------------------------------------------------- /infra/clouddeploy.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: deploy.cloud.google.com/v1beta1 16 | kind: DeliveryPipeline 17 | metadata: 18 | name: criticality-score-deploy 19 | description: Pipeline for deploying Criticality Score 20 | serialPipeline: 21 | stages: 22 | - targetId: staging-target 23 | profiles: 24 | - staging 25 | - targetId: prod-target 26 | profiles: 27 | - prod 28 | 29 | --- 30 | 31 | apiVersion: deploy.cloud.google.com/v1beta1 32 | kind: Target 33 | metadata: 34 | name: staging-target 35 | description: Staging deploy target 36 | gke: 37 | cluster: projects/openssf/locations/us-central1-c/clusters/criticality-score 38 | 39 | --- 40 | 41 | apiVersion: deploy.cloud.google.com/v1beta1 42 | kind: Target 43 | metadata: 44 | name: prod-target 45 | description: Production deploy target 46 | gke: 47 | cluster: projects/openssf/locations/us-central1-c/clusters/criticality-score 48 | requireApproval: true 49 | -------------------------------------------------------------------------------- /infra/envs/base/bq_transfer.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: batch/v1 16 | kind: CronJob 17 | metadata: 18 | name: criticality-score-bq-transfer 19 | spec: 20 | # At 3:00UTC, 13:00AEST on Wednesday and Friday. 21 | schedule: "0 3 * * 3,5" 22 | concurrencyPolicy: "Forbid" 23 | jobTemplate: 24 | spec: 25 | template: 26 | spec: 27 | containers: 28 | - name: bq-transfer 29 | image: scorecard-bq-transfer 30 | args: ["--config=/etc/criticality_score/config.yaml"] 31 | imagePullPolicy: Always 32 | volumeMounts: 33 | - name: config-volume 34 | mountPath: /etc/criticality_score 35 | readOnly: true 36 | volumes: 37 | - name: config-volume 38 | configMap: 39 | name: criticality-score-config 40 | restartPolicy: OnFailure 41 | -------------------------------------------------------------------------------- /infra/envs/base/csv_transfer.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: batch/v1 16 | kind: CronJob 17 | metadata: 18 | name: criticality-score-csv-transfer 19 | spec: 20 | # At 3:00UTC, 13:00AEST on Wednesday and Friday. 21 | schedule: "0 3 * * 3,5" 22 | concurrencyPolicy: "Forbid" 23 | jobTemplate: 24 | spec: 25 | template: 26 | spec: 27 | containers: 28 | - name: bq-transfer 29 | image: csv-transfer 30 | args: ["--config=/etc/criticality_score/config.yaml"] 31 | imagePullPolicy: Always 32 | volumeMounts: 33 | - name: config-volume 34 | mountPath: /etc/criticality_score 35 | readOnly: true 36 | volumes: 37 | - name: config-volume 38 | configMap: 39 | name: criticality-score-config 40 | restartPolicy: OnFailure 41 | -------------------------------------------------------------------------------- /infra/envs/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | resources: 16 | - bq_transfer.yaml 17 | - csv_transfer.yaml 18 | - worker.yaml 19 | 20 | # Dummy config map to ensure the label below is applied. 21 | configMapGenerator: 22 | - name: criticality-score-config 23 | 24 | # Add an identifying label to allow pruning of unused resources (i.e. ConfigMaps). 25 | commonLabels: 26 | managed-by-kustomize: "true" 27 | 28 | # Hardcode versions to match go.mod for github.com/ossf/scorecard. 29 | images: 30 | - name: scorecard-github-server 31 | newName: gcr.io/openssf/scorecard-github-server 32 | newTag: 49c0eed3a423f00c872b5c3c9f1bbca9e8aae799 33 | - name: scorecard-bq-transfer 34 | newName: gcr.io/openssf/scorecard-bq-transfer 35 | newTag: 49c0eed3a423f00c872b5c3c9f1bbca9e8aae799 36 | -------------------------------------------------------------------------------- /infra/envs/base/worker.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: batch/v1 16 | kind: CronJob 17 | metadata: 18 | name: criticality-score-batch-worker 19 | spec: 20 | # The 1st and 15th of every month at 23:00UTC, or 9:00AEST. 21 | schedule: "0 23 1,15 * *" 22 | concurrencyPolicy: "Forbid" 23 | 24 | jobTemplate: 25 | spec: 26 | template: 27 | metadata: 28 | labels: 29 | app.kubernetes.io/name: worker 30 | spec: 31 | restartPolicy: OnFailure 32 | initContainers: 33 | - name: aggregate-repos 34 | image: init-collect-signals 35 | command: ['/bin/init.sh', '/etc/criticality_score/config.yaml'] 36 | imagePullPolicy: Always 37 | volumeMounts: 38 | - name: config-volume 39 | mountPath: /etc/criticality_score 40 | readOnly: true 41 | - name: shared-storage 42 | mountPath: /shared 43 | containers: 44 | - name: worker 45 | image: collect-signals 46 | args: ["--config=/etc/criticality_score/config.yaml"] 47 | imagePullPolicy: Always 48 | resources: 49 | requests: 50 | ephemeral-storage: 100Gi 51 | limits: 52 | ephemeral-storage: 500Gi 53 | volumeMounts: 54 | - name: config-volume 55 | mountPath: /etc/criticality_score 56 | readOnly: true 57 | - name: shared-storage 58 | mountPath: /shared 59 | volumes: 60 | - name: config-volume 61 | configMap: 62 | name: criticality-score-config 63 | - name: shared-storage 64 | emptyDir: 65 | sizeLimit: 500Mi 66 | -------------------------------------------------------------------------------- /infra/envs/prod/config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | project-id: openssf 16 | request-topic-url: gcppubsub://projects/openssf/topics/criticality-score-batch-requests 17 | request-subscription-url: gcppubsub://projects/openssf/subscriptions/criticality-score-batch-worker 18 | bigquery-dataset: criticality_score_cron 19 | bigquery-table: criticality-score-v0 20 | completion-threshold: 0.99 21 | shard-size: 10 22 | webhook-url: 23 | metric-exporter: stackdriver 24 | metric-stackdriver-prefix: criticality-score-cron 25 | result-data-bucket-url: gs://ossf-criticality-score-data 26 | 27 | additional-params: 28 | input-bucket: 29 | url: gs://ossf-criticality-score-url-data 30 | prefix-file: latest 31 | 32 | criticality: 33 | log-env: gcp 34 | log-level: info 35 | dataset: ossf_criticality_score_depsdev 36 | dataset-ttl-hours: 672 # 4 weeks 37 | scoring: enabled 38 | scoring-config: config/scorer/pike_depsdev.yml 39 | scoring-column-name: default_score 40 | csv-transfer-bucket-url: gs://ossf-criticality-score 41 | csv-transfer-filename: all.csv 42 | local: enabled 43 | local-url-data-file: /shared/repos.txt 44 | local-state-file: /shared/state.json 45 | 46 | scorecard: 47 | # Use the raw-result-data-bucket-url in place of a better option. 48 | # The controller will write the shard metadata to this bucket. 49 | # TODO: use a more sensible configuration entry 50 | raw-result-data-bucket-url: gs://ossf-criticality-score-csv-data 51 | -------------------------------------------------------------------------------- /infra/envs/prod/github_auth.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: batch/v1 16 | kind: CronJob 17 | metadata: 18 | name: criticality-score-batch-worker 19 | spec: 20 | jobTemplate: 21 | spec: 22 | template: 23 | spec: 24 | containers: 25 | - name: worker 26 | env: 27 | - name: GITHUB_APP_KEY_PATH 28 | value: /etc/github/app_key 29 | - name: GITHUB_APP_ID 30 | valueFrom: 31 | secretKeyRef: 32 | name: github 33 | key: app_id 34 | - name: GITHUB_APP_INSTALLATION_ID 35 | valueFrom: 36 | secretKeyRef: 37 | name: github 38 | key: installation_id 39 | volumeMounts: 40 | - name: github-app-key 41 | mountPath: "/etc/github/" 42 | readOnly: true 43 | volumes: 44 | - name: github-app-key 45 | secret: 46 | secretName: github 47 | -------------------------------------------------------------------------------- /infra/envs/prod/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | bases: 16 | - ../base 17 | 18 | # Add identifying labels for the prod environment. 19 | commonLabels: 20 | env: prod 21 | 22 | # Use a staging specific configuration file. 23 | configMapGenerator: 24 | - name: criticality-score-config 25 | behavior: replace 26 | files: 27 | - config.yaml 28 | 29 | # Patches customize the base configurations for the prod environment. 30 | patchesStrategicMerge: 31 | - github_auth.yaml 32 | -------------------------------------------------------------------------------- /infra/envs/staging/bq_transfer_schedule.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: batch/v1 16 | kind: CronJob 17 | metadata: 18 | name: criticality-score-bq-transfer 19 | spec: 20 | # At 4:00UTC, 14:00AEST each weekday. 21 | schedule: "0 4 * * 1-5" 22 | -------------------------------------------------------------------------------- /infra/envs/staging/config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | project-id: openssf 16 | request-topic-url: gcppubsub://projects/openssf/topics/criticality-score-batch-requests-staging 17 | request-subscription-url: gcppubsub://projects/openssf/subscriptions/criticality-score-batch-worker-staging 18 | bigquery-dataset: criticality_score_cron 19 | bigquery-table: criticality-score-v0-staging 20 | completion-threshold: 0.99 21 | shard-size: 10 22 | webhook-url: 23 | metric-exporter: stackdriver 24 | metric-stackdriver-prefix: criticality-score-cron-staging 25 | result-data-bucket-url: gs://ossf-criticality-score-data-staging 26 | 27 | additional-params: 28 | input-bucket: 29 | url: gs://ossf-criticality-score-url-data-staging 30 | prefix-file: latest 31 | 32 | criticality: 33 | log-env: gcp 34 | log-level: info 35 | dataset: ossf_criticality_score_depsdev_staging 36 | dataset-ttl-hours: 168 # 1 week 37 | scoring: enabled 38 | scoring-config: config/scorer/pike_depsdev.yml 39 | scoring-column-name: default_score 40 | csv-transfer-bucket-url: gs://ossf-criticality-score-staging 41 | csv-transfer-filename: all.csv 42 | local: enabled 43 | local-url-data-file: /shared/repos.txt 44 | local-state-file: /shared/state.json 45 | 46 | scorecard: 47 | # Use the raw-result-data-bucket-url in place of a better option. 48 | # The controller will write the shard metadata to this bucket. 49 | # TODO: use a more sensible configuration entry 50 | raw-result-data-bucket-url: gs://ossf-criticality-score-csv-data-staging 51 | -------------------------------------------------------------------------------- /infra/envs/staging/controller_schedule.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: batch/v1 16 | kind: CronJob 17 | metadata: 18 | name: criticality-score-batch-worker 19 | spec: 20 | # At 0:30UTC, or 10:30AEST each weekday. 21 | schedule: "30 0 * * 1-5" 22 | -------------------------------------------------------------------------------- /infra/envs/staging/csv_transfer_schedule.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: batch/v1 16 | kind: CronJob 17 | metadata: 18 | name: criticality-score-csv-transfer 19 | spec: 20 | # At 4:00UTC, 14:00AEST each weekday. 21 | schedule: "0 4 * * 1-5" 22 | -------------------------------------------------------------------------------- /infra/envs/staging/github_auth.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: batch/v1 16 | kind: CronJob 17 | metadata: 18 | name: criticality-score-batch-worker 19 | spec: 20 | jobTemplate: 21 | spec: 22 | template: 23 | spec: 24 | containers: 25 | - name: worker 26 | env: 27 | - name: GITHUB_AUTH_TOKEN 28 | valueFrom: 29 | secretKeyRef: 30 | name: github-staging 31 | key: token 32 | -------------------------------------------------------------------------------- /infra/envs/staging/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | bases: 16 | - ../base 17 | 18 | # Add identifying labels for the staging environment. 19 | nameSuffix: -staging 20 | commonLabels: 21 | env: staging 22 | 23 | # Use a staging specific configuration file. 24 | configMapGenerator: 25 | - name: criticality-score-config 26 | behavior: replace 27 | files: 28 | - config.yaml 29 | 30 | # Patches customize the base configurations for the staging environment. 31 | patchesStrategicMerge: 32 | - github_auth.yaml 33 | - controller_schedule.yaml 34 | - csv_transfer_schedule.yaml 35 | - bq_transfer_schedule.yaml 36 | -------------------------------------------------------------------------------- /infra/images/init_collect_signals/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM gcr.io/google.com/cloudsdktool/google-cloud-cli:slim@sha256:e151ce4405e7f71c1a2ba8921af99e83c6cb7d8c4fc5e49d94c2807889b57564 16 | 17 | # Add "yq" to the image so the YAML config can be read. 18 | RUN apt-get update -qqy && apt-get install -qqy yq 19 | 20 | WORKDIR /bin 21 | COPY ./infra/images/init_collect_signals/init.sh ./ 22 | RUN chmod u+x init.sh 23 | -------------------------------------------------------------------------------- /infra/images/init_collect_signals/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright 2023 Criticality Score Authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # Test usage (from the base dir of the repo): 17 | # docker build . -f containers/init_collect_signals/Dockerfile -t criticality_score_init_collection 18 | # docker run 19 | # -v /tmp:/output \ 20 | # -v $HOME/.config/gcloud:/root/.config/gcloud \ 21 | # -v $HOME/path/to/config.yaml:/etc/config.yaml \ 22 | # -ti criticality_score_init_collection \ 23 | # /bin/init.sh /etc/config.yaml 24 | 25 | CONFIG_FILE="$1" 26 | 27 | # Read the appropriate settings from the YAML config file. 28 | BUCKET_URL=`yq -r '."additional-params"."input-bucket".url' "$CONFIG_FILE"` 29 | BUCKET_PREFIX_FILE=`yq -r '."additional-params"."input-bucket"."prefix-file"' "$CONFIG_FILE"` 30 | OUTPUT_FILE=`yq -r '."additional-params".criticality."local-url-data-file"' "$CONFIG_FILE"` 31 | echo "bucket url = $BUCKET_URL" 32 | echo "bucket prefix file = $BUCKET_PREFIX_FILE" 33 | echo "url data file = $OUTPUT_FILE" 34 | 35 | # Exit early if $OUTPUT_FILE already exists. This ensures $OUTPUT_FILE remains 36 | # stable across any restart. Since the file is created atomically we can be 37 | # confident that the file won't be corrupt or truncated. 38 | if [ -f "$OUTPUT_FILE" ]; then 39 | echo "Exiting early. $OUTPUT_FILE already exists." 40 | exit 0 41 | fi 42 | 43 | LATEST_PREFIX=`gsutil cat "$BUCKET_URL"/"$BUCKET_PREFIX_FILE"` 44 | echo "latest prefix = $LATEST_PREFIX" 45 | 46 | # Deinfe some temporary files based on OUTPUT_FILE so they're on the same volume. 47 | TMP_OUTPUT_FILE_1="$OUTPUT_FILE-tmp-1" 48 | TMP_OUTPUT_FILE_2="$OUTPUT_FILE-tmp-2" 49 | 50 | # Iterate through all the files to merge all together. 51 | touch "$TMP_OUTPUT_FILE_1" 52 | for file in `gsutil ls "$BUCKET_URL"/"$LATEST_PREFIX"`; do 53 | echo "reading $file" 54 | # Read the file, remove the header and turn it into a plain list of repos. 55 | gsutil cat "$file" | tail -n +2 | cut -d',' -f1 >> "$TMP_OUTPUT_FILE_1" 56 | done 57 | 58 | # Ensure the file contains only one entry per repo, and shuffle it. 59 | sort "$TMP_OUTPUT_FILE_1" | uniq | shuf > "$TMP_OUTPUT_FILE_2" 60 | rm "$TMP_OUTPUT_FILE_1" 61 | 62 | # Move the final tmp file to the output file to ensure the change is atomic. 63 | mv "$TMP_OUTPUT_FILE_2" "$OUTPUT_FILE" 64 | 65 | echo "wrote $OUTPUT_FILE" 66 | -------------------------------------------------------------------------------- /infra/k8s/enumerate_github.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: batch/v1 16 | kind: CronJob 17 | metadata: 18 | name: criticality-score-enumerate-github 19 | spec: 20 | # Run twice weekly, on Sunday and Wednesday, at 23:00UTC for >=20 stars. 21 | schedule: "0 23 * * 0,3" 22 | concurrencyPolicy: "Forbid" 23 | jobTemplate: 24 | spec: 25 | template: 26 | spec: 27 | containers: 28 | - name: enumerate-github 29 | image: gcr.io/openssf/criticality-score-enumerate-github:latest 30 | imagePullPolicy: Always 31 | env: 32 | - name: GITHUB_TOKEN 33 | valueFrom: 34 | secretKeyRef: 35 | name: enumerate-github-auth 36 | key: token 37 | - name: CRITICALITY_SCORE_LOG_ENV 38 | value: "gcp" 39 | - name: CRITICALITY_SCORE_OUTFILE 40 | value: "gs://ossf-criticality-score-url-data/[[runid]]/github.csv" 41 | - name: CRITICALITY_SCORE_OUTFILE_FORCE 42 | value: "1" 43 | - name: CRITICALITY_SCORE_MARKER 44 | value: "gs://ossf-criticality-score-url-data/latest" 45 | - name: CRITICALITY_SCORE_MARKER_TYPE 46 | value: "dir" 47 | - name: CRITICALITY_SCORE_STARS_MIN 48 | value: "60" 49 | - name: CRITICALITY_SCORE_START_DATE 50 | value: "2008-01-01" 51 | - name: CRITICALITY_SCORE_FORMAT 52 | value: "scorecard" 53 | resources: 54 | limits: 55 | memory: 5Gi 56 | requests: 57 | memory: 2Gi 58 | restartPolicy: OnFailure 59 | -------------------------------------------------------------------------------- /infra/skaffold.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: skaffold/v2beta29 16 | kind: Config 17 | profiles: 18 | - name: staging 19 | deploy: 20 | kustomize: 21 | paths: 22 | - envs/staging 23 | flags: 24 | apply: ['--prune', '-l', 'managed-by-kustomize=true,env=staging'] 25 | - name: prod 26 | deploy: 27 | kustomize: 28 | paths: 29 | - envs/prod 30 | flags: 31 | apply: ['--prune', '-l', 'managed-by-kustomize=true,env=prod'] 32 | -------------------------------------------------------------------------------- /infra/test/README.md: -------------------------------------------------------------------------------- 1 | # Test infrastructure using docker-compose 2 | 3 | This infrastructure is used during development, for local execution, or to 4 | verify the various components work together in a cloud environment. 5 | 6 | ## Requirements 7 | 8 | - Docker and Docker Compose are installed 9 | ([docs](https://docs.docker.com/compose/install/)) 10 | - Docker images have been built. To build them, run: 11 | 12 | ```shell 13 | $ make build/docker 14 | ``` 15 | 16 | - Environment variable `GITHUB_TOKEN` set with a GitHub personal access token. 17 | 18 | ## Usage 19 | 20 | To start in daemon mode, run: 21 | 22 | ```shell 23 | $ docker-compose up -d 24 | ``` 25 | 26 | To stop, run: 27 | 28 | ```shell 29 | $ docker-compose down 30 | ``` 31 | 32 | To view logs, run: 33 | 34 | ```shell 35 | $ docker-compose logs -f 36 | ``` 37 | 38 | To restart enumerating github, run: 39 | 40 | ``` 41 | $ docker-compose start enumerate-github 42 | ``` 43 | 44 | To access the storage bucket, visit http://localhost:9000/ in a browser, with 45 | username `minio` and password `minio123`. 46 | 47 | ## Notes 48 | 49 | By default it only enumerates GitHub repositories with 100 or more stars, but 50 | this can be set using the `CRITICALITY_SCORE_STARS_MIN` environment variable. 51 | -------------------------------------------------------------------------------- /infra/test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Development configuration of Criticality Score infrastructure. 16 | version: "3.9" 17 | services: 18 | minio: 19 | image: minio/minio@sha256:684ce208c005fe032659ec77bafa6a17a16c41686c334618dec924b3505e7090 20 | hostname: minio 21 | ports: 22 | - 9000:9000 23 | environment: 24 | MINIO_ROOT_USER: minio 25 | MINIO_ROOT_PASSWORD: minio123 26 | MINIO_REGION_NAME: dummy_region 27 | entrypoint: sh 28 | command: -c 'mkdir -p /data/criticality_score && /usr/bin/minio server /data' 29 | healthcheck: 30 | test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] 31 | interval: 30s 32 | timeout: 20s 33 | retries: 3 34 | 35 | auth: 36 | image: gcr.io/openssf/scorecard-github-server:latest 37 | hostname: auth 38 | environment: 39 | GITHUB_AUTH_TOKEN: "${GITHUB_TOKEN:?required}" 40 | ports: 41 | - 8080:8080 42 | 43 | enumerate-github: 44 | image: criticality-score-enumerate-github:latest 45 | environment: 46 | CRITICALITY_SCORE_OUTFILE: s3://criticality_score/enumeration/[[runid]]/github.txt?endpoint=minio:9000&disableSSL=true&s3ForcePathStyle=true 47 | CRITICALITY_SCORE_OUTFILE_FORCE: 1 48 | CRITICALITY_SCORE_STARS_MIN: ${CRITICALITY_SCORE_STARS_MIN:-100} 49 | CRITICALITY_SCORE_START_DATE: ${CRITICALITY_SCORE_START_DATE} 50 | CRITICALITY_SCORE_END_DATE: ${CRITICALITY_SCORE_END_DATE} 51 | CRITICALITY_SCORE_MARKER: s3://criticality_score/enumeration/latest.txt?endpoint=minio:9000&disableSSL=true&s3ForcePathStyle=true 52 | CRITICALITY_SCORE_MARKER_TYPE: dir 53 | GITHUB_AUTH_SERVER: auth:8080 54 | AWS_ACCESS_KEY_ID: minio 55 | AWS_SECRET_ACCESS_KEY: minio123 56 | AWS_REGION: dummy_region 57 | restart: on-failure 58 | depends_on: 59 | - minio 60 | - auth 61 | -------------------------------------------------------------------------------- /internal/cloudstorage/cloudstorage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cloudstorage 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "io" 21 | "net/url" 22 | "path" 23 | "strings" 24 | 25 | "gocloud.dev/blob" 26 | _ "gocloud.dev/blob/fileblob" 27 | _ "gocloud.dev/blob/gcsblob" 28 | _ "gocloud.dev/blob/memblob" 29 | _ "gocloud.dev/blob/s3blob" 30 | ) 31 | 32 | const fileScheme = "file" 33 | 34 | func parseBucketAndPrefix(rawURL string) (bucket, prefix string, _ error) { 35 | u, err := url.Parse(rawURL) 36 | if err != nil { 37 | return "", "", fmt.Errorf("url parse: %w", err) 38 | } 39 | 40 | // If the URL doesn't have a scheme it is possibly a local file. Use the 41 | // fileblob storage to handle these files because the behavior is 42 | // more consistent with cloud storage services - in particular 43 | // atomic updates, and read-after-write consistency. 44 | if !u.IsAbs() { 45 | // If the Host is set (e.g. //example.com/) then we have a problem. 46 | if u.Host != "" { 47 | return "", "", fmt.Errorf("undefined blob scheme: %s", u.String()) 48 | } 49 | 50 | // Assume a scheme-less, host-less url is a local file. 51 | u.Scheme = fileScheme 52 | if !path.IsAbs(u.Path) { 53 | u.Host = "." 54 | } 55 | // Turn off .attrs files, becaue they look weird next to local files. 56 | q := u.Query() 57 | q.Set("metadata", "skip") 58 | u.RawQuery = q.Encode() 59 | } 60 | 61 | if u.Scheme == fileScheme { 62 | // File schemes are treated differently, as the dir forms the bucket. 63 | u.Path, prefix = path.Split(u.Path) 64 | } else { 65 | prefix = strings.TrimPrefix(u.Path, "/") 66 | u.Path = "" 67 | } 68 | 69 | bucket = u.String() 70 | return bucket, prefix, nil 71 | } 72 | 73 | func NewWriter(ctx context.Context, rawURL string) (io.WriteCloser, error) { 74 | bucket, prefix, err := parseBucketAndPrefix(rawURL) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | b, err := blob.OpenBucket(ctx, bucket) 80 | if err != nil { 81 | return nil, fmt.Errorf("failed opening %s: %w", bucket, err) 82 | } 83 | w, err := b.NewWriter(ctx, prefix, nil) 84 | if err != nil { 85 | return nil, fmt.Errorf("failed creating writer for %s: %w", rawURL, err) 86 | } 87 | return w, nil 88 | } 89 | -------------------------------------------------------------------------------- /internal/collector/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package collector 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | "time" 21 | 22 | "go.uber.org/zap/zaptest" 23 | ) 24 | 25 | var allSourceTypes = []SourceType{ 26 | SourceTypeGithubRepo, 27 | SourceTypeGithubIssues, 28 | SourceTypeGitHubMentions, 29 | SourceTypeDepsDev, 30 | } 31 | 32 | func TestIsEnabled_AllEnabled(t *testing.T) { 33 | c := makeTestConfig(t, EnableAllSources()) 34 | for _, sourceType := range allSourceTypes { 35 | t.Run(sourceType.String(), func(t *testing.T) { 36 | if !c.IsEnabled(sourceType) { 37 | t.Fatalf("IsEnabled(%s) = false, want true", sourceType) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func TestIsEnabled_AllDisabled(t *testing.T) { 44 | c := makeTestConfig(t, DisableAllSources()) 45 | for _, sourceType := range allSourceTypes { 46 | t.Run(sourceType.String(), func(t *testing.T) { 47 | if c.IsEnabled(sourceType) { 48 | t.Fatalf("IsEnabled(%s) = true, want false", sourceType) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | func TestIsEnabled_OneDisabled(t *testing.T) { 55 | c := makeTestConfig(t, EnableAllSources(), DisableSource(SourceTypeDepsDev)) 56 | if c.IsEnabled(SourceTypeDepsDev) { 57 | t.Fatalf("IsEnabled(%s) = true, want false", SourceTypeDepsDev) 58 | } 59 | } 60 | 61 | func TestIsEnabled_OneEnabled(t *testing.T) { 62 | c := makeTestConfig(t, DisableAllSources(), EnableSource(SourceTypeDepsDev)) 63 | if !c.IsEnabled(SourceTypeDepsDev) { 64 | t.Fatalf("IsEnabled(%s) = false, want true", SourceTypeDepsDev) 65 | } 66 | } 67 | 68 | func TestGCPProject(t *testing.T) { 69 | want := "my-project-id" 70 | c := makeTestConfig(t, GCPProject(want)) 71 | if c.gcpProject != want { 72 | t.Fatalf("config.gcpProject = %q, want %q", c.gcpProject, want) 73 | } 74 | } 75 | 76 | func TestGCPDatasetName(t *testing.T) { 77 | want := "my-dataset-name" 78 | c := makeTestConfig(t, GCPDatasetName(want)) 79 | if c.gcpDatasetName != want { 80 | t.Fatalf("config.gcpDatasetName = %q, want %q", c.gcpDatasetName, want) 81 | } 82 | } 83 | 84 | func TestGCPDatasetTTL(t *testing.T) { 85 | want := time.Duration(24) * time.Hour 86 | c := makeTestConfig(t, GCPDatasetTTL(want)) 87 | if c.gcpDatasetTTL != want { 88 | t.Fatalf("config.gcpDatasetTTL = %q, want %q", c.gcpDatasetTTL, want) 89 | } 90 | } 91 | 92 | func makeTestConfig(t *testing.T, opts ...Option) *config { 93 | t.Helper() 94 | return makeConfig(context.Background(), zaptest.NewLogger(t), opts...) 95 | } 96 | -------------------------------------------------------------------------------- /internal/collector/depsdev/source.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package depsdev 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "net/url" 21 | "strings" 22 | "time" 23 | 24 | "cloud.google.com/go/bigquery" 25 | "go.uber.org/zap" 26 | 27 | "github.com/ossf/criticality_score/v2/internal/collector/projectrepo" 28 | "github.com/ossf/criticality_score/v2/internal/collector/signal" 29 | ) 30 | 31 | const ( 32 | defaultLocation = "US" 33 | ) 34 | 35 | type depsDevSet struct { 36 | DependentCount signal.Field[int] `signal:"dependent_count"` 37 | } 38 | 39 | func (s *depsDevSet) Namespace() signal.Namespace { 40 | return "depsdev" 41 | } 42 | 43 | type depsDevSource struct { 44 | logger *zap.Logger 45 | dependents *dependents 46 | } 47 | 48 | func (c *depsDevSource) EmptySet() signal.Set { 49 | return &depsDevSet{} 50 | } 51 | 52 | func (c *depsDevSource) IsSupported(r projectrepo.Repo) bool { 53 | _, t := parseRepoURL(r.URL()) 54 | return t != "" 55 | } 56 | 57 | func (c *depsDevSource) Get(ctx context.Context, r projectrepo.Repo, jobID string) (signal.Set, error) { 58 | var s depsDevSet 59 | n, t := parseRepoURL(r.URL()) 60 | if t == "" { 61 | return &s, nil 62 | } 63 | c.logger.With(zap.String("url", r.URL().String())).Debug("Fetching deps.dev dependent count") 64 | deps, found, err := c.dependents.Count(ctx, n, t, jobID) 65 | if err != nil { 66 | return nil, fmt.Errorf("failed to fetch deps.dev dependent count: %w", err) 67 | } 68 | if found { 69 | s.DependentCount.Set(deps) 70 | } 71 | return &s, nil 72 | } 73 | 74 | // NewSource creates a new Source for gathering data from deps.dev. 75 | // 76 | // TODO add options to configure the dataset: 77 | // - force dataset re-creation (-update-strategy = always,stale,weekly,monthly,never) 78 | // - force dataset destruction (-depsdev-destroy-data) 79 | func NewSource(ctx context.Context, logger *zap.Logger, projectID, datasetName string, datasetTTL time.Duration) (signal.Source, error) { 80 | if projectID == "" { 81 | projectID = bigquery.DetectProjectID 82 | } 83 | gcpClient, err := bigquery.NewClient(ctx, projectID) 84 | if err != nil { 85 | return nil, fmt.Errorf("failed to create bigquery client: %w", err) 86 | } 87 | // Set the location 88 | gcpClient.Location = defaultLocation 89 | 90 | dependents, err := NewDependents(ctx, gcpClient, logger, datasetName, datasetTTL) 91 | if err != nil { 92 | return nil, fmt.Errorf("failed to create deps.dev dependents: %w", err) 93 | } 94 | 95 | return &depsDevSource{ 96 | logger: logger, 97 | dependents: dependents, 98 | }, nil 99 | } 100 | 101 | func parseRepoURL(u *url.URL) (projectName, projectType string) { 102 | switch hn := u.Hostname(); hn { 103 | case "github.com": 104 | return strings.Trim(u.Path, "/"), "GITHUB" 105 | default: 106 | return "", "" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /internal/collector/github/factory.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package github 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "net/url" 22 | 23 | "go.uber.org/zap" 24 | 25 | "github.com/ossf/criticality_score/v2/internal/collector/projectrepo" 26 | "github.com/ossf/criticality_score/v2/internal/githubapi" 27 | ) 28 | 29 | type factory struct { 30 | client *githubapi.Client 31 | logger *zap.Logger 32 | } 33 | 34 | func NewRepoFactory(client *githubapi.Client, logger *zap.Logger) projectrepo.Factory { 35 | return &factory{ 36 | client: client, 37 | logger: logger, 38 | } 39 | } 40 | 41 | func (f *factory) New(ctx context.Context, u *url.URL) (projectrepo.Repo, error) { 42 | r := &repo{ 43 | client: f.client, 44 | origURL: u, 45 | logger: f.logger.With(zap.String("url", u.String())), 46 | } 47 | if err := r.init(ctx); err != nil { 48 | if errors.Is(err, githubapi.ErrGraphQLNotFound) { 49 | // TODO: replace %v with %w after upgrading Go from 1.19 to 1.21 50 | return nil, fmt.Errorf("%w (%s): %v", projectrepo.ErrNoRepoFound, u, err) 51 | } else if errors.Is(err, githubapi.ErrGraphQLForbidden) { 52 | // TODO: replace %v with %w after upgrading Go from 1.19 to 1.21 53 | return nil, fmt.Errorf("%w (%s): %v", projectrepo.ErrRepoInaccessible, u, err) 54 | } else { 55 | return nil, err 56 | } 57 | } 58 | return r, nil 59 | } 60 | 61 | func (f *factory) Match(u *url.URL) bool { 62 | return u.Hostname() == "github.com" 63 | } 64 | -------------------------------------------------------------------------------- /internal/collector/github/legacy/constants.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package legacy 16 | 17 | import ( 18 | "errors" 19 | "time" 20 | ) 21 | 22 | const ( 23 | SinceDuration time.Duration = time.Hour * 24 * 30 24 | IssueLookback time.Duration = time.Hour * 24 * 90 25 | 26 | // TODO: these limits should ultimately be imposed by the score generation, not here. 27 | MaxContributorLimit = 5000 28 | MaxIssuesLimit = 5000 29 | MaxTopContributors = 15 30 | 31 | TooManyContributorsOrgCount = 10 32 | TooManyCommentsFrequency = 2.0 33 | 34 | releasesPerPage = 100 35 | ) 36 | 37 | var ErrorTooManyResults = errors.New("too many results") 38 | -------------------------------------------------------------------------------- /internal/collector/github/legacy/created.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package legacy 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "time" 21 | 22 | "github.com/google/go-github/v47/github" 23 | 24 | "github.com/ossf/criticality_score/v2/internal/githubapi" 25 | ) 26 | 27 | // FetchCreatedTime returns the earliest known creation time for a given 28 | // repository based on the commit history, before or equal to earliestSoFar. 29 | // 30 | // Only times before earliestSoFar will be considered. If there is no time before 31 | // earliestSoFar found, the value of earliestSoFar will be returned. 32 | func FetchCreatedTime(ctx context.Context, c *githubapi.Client, owner, name string, earliestSoFar time.Time) (time.Time, error) { 33 | opts := &github.CommitsListOptions{ 34 | Until: earliestSoFar, 35 | ListOptions: github.ListOptions{PerPage: 1}, // 1 result per page means LastPage is total number of records. 36 | } 37 | cs, resp, err := c.Rest().Repositories.ListCommits(ctx, owner, name, opts) 38 | if githubapi.ErrorResponseStatusCode(err) == 409 { 39 | // 409 Conflict can happen if the Git Repository is empty. 40 | return earliestSoFar, nil 41 | } 42 | if err != nil { 43 | return time.Time{}, err 44 | } 45 | // Handle 0 or 1 result. 46 | if resp.NextPage == 0 || resp.LastPage == 1 { 47 | if len(cs) == 0 { 48 | return earliestSoFar, nil 49 | } else { 50 | return cs[0].GetCommit().GetCommitter().GetDate(), nil 51 | } 52 | } 53 | // It is possible that new commits are pushed between the previous 54 | // request and the next. If we detect that we are not on LastPage 55 | // try again a few more times. 56 | attempts := 5 57 | for opts.Page != resp.LastPage && attempts > 0 { 58 | opts.Page = resp.LastPage 59 | cs, resp, err = c.Rest().Repositories.ListCommits(ctx, owner, name, opts) 60 | if err != nil { 61 | return time.Time{}, err 62 | } 63 | attempts-- 64 | } 65 | if len(cs) != 0 { 66 | return cs[len(cs)-1].GetCommit().GetCommitter().GetDate(), nil 67 | } else { 68 | return time.Time{}, fmt.Errorf("commits disappeared for GitHub repo '%s/%s'", owner, name) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /internal/collector/github/legacy/issues.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package legacy 16 | 17 | import ( 18 | "context" 19 | "time" 20 | 21 | "github.com/google/go-github/v47/github" 22 | 23 | "github.com/ossf/criticality_score/v2/internal/githubapi" 24 | ) 25 | 26 | type IssueState string 27 | 28 | const ( 29 | IssueStateAll = "all" 30 | IssueStateOpen = "open" 31 | IssueStateClosed = "closed" 32 | ) 33 | 34 | // FetchIssueCount returns the total number of issues for a given repo in a 35 | // given state, across the past lookback duration. 36 | // 37 | // This count includes both issues and pull requests. 38 | func FetchIssueCount(ctx context.Context, c *githubapi.Client, owner, name string, state IssueState, lookback time.Duration) (int, error) { 39 | opts := &github.IssueListByRepoOptions{ 40 | Since: time.Now().UTC().Add(-lookback), 41 | State: string(state), 42 | ListOptions: github.ListOptions{PerPage: 1}, // 1 result per page means LastPage is total number of records. 43 | } 44 | is, resp, err := c.Rest().Issues.ListByRepo(ctx, owner, name, opts) 45 | // The API returns 5xx responses if there are too many issues. 46 | if c := githubapi.ErrorResponseStatusCode(err); 500 <= c && c < 600 { 47 | return MaxIssuesLimit, nil 48 | } 49 | if err != nil { 50 | return 0, err 51 | } 52 | if resp.NextPage == 0 { 53 | return len(is), nil 54 | } 55 | return resp.LastPage, nil 56 | } 57 | 58 | // FetchIssueCommentCount returns the total number of comments for a given repo 59 | // across all issues and pull requests, for the past lookback duration. 60 | // 61 | // If the exact number if unable to be returned because there are too many 62 | // results, a TooManyResultsError will be returned. 63 | func FetchIssueCommentCount(ctx context.Context, c *githubapi.Client, owner, name string, lookback time.Duration) (int, error) { 64 | since := time.Now().UTC().Add(-lookback) 65 | opts := &github.IssueListCommentsOptions{ 66 | Since: &since, 67 | ListOptions: github.ListOptions{PerPage: 1}, // 1 result per page means LastPage is total number of records. 68 | } 69 | cs, resp, err := c.Rest().Issues.ListComments(ctx, owner, name, 0, opts) 70 | // The API returns 5xx responses if there are too many comments. 71 | if c := githubapi.ErrorResponseStatusCode(err); 500 <= c && c < 600 { 72 | return 0, ErrorTooManyResults 73 | } 74 | if err != nil { 75 | return 0, err 76 | } 77 | if resp.NextPage == 0 { 78 | return len(cs), nil 79 | } 80 | return resp.LastPage, nil 81 | } 82 | -------------------------------------------------------------------------------- /internal/collector/github/legacy/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package legacy 16 | 17 | import ( 18 | "math" 19 | "time" 20 | ) 21 | 22 | // empty is a convenience wrapper for the empty struct. 23 | type empty struct{} 24 | 25 | func TimeDelta(a, b time.Time, u time.Duration) int { 26 | var d time.Duration 27 | if a.Before(b) { 28 | d = b.Sub(a) 29 | } else { 30 | d = a.Sub(b) 31 | } 32 | return int(d / u) 33 | } 34 | 35 | // Round will return v approximately rounded to a precision of p decimal places. 36 | func Round(v float64, p int) float64 { 37 | m := math.Pow10(p) 38 | return math.Round(v*m) / m 39 | } 40 | -------------------------------------------------------------------------------- /internal/collector/github/queries.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package github 16 | 17 | import ( 18 | "context" 19 | "net/url" 20 | "strings" 21 | "time" 22 | 23 | "github.com/hasura/go-graphql-client" 24 | 25 | "github.com/ossf/criticality_score/v2/internal/githubapi" 26 | ) 27 | 28 | const ( 29 | legacyReleaseLookbackDays = 365 30 | legacyReleaseLookback = legacyReleaseLookbackDays * 24 * time.Hour 31 | legacyCommitLookback = 365 * 24 * time.Hour 32 | ) 33 | 34 | type basicRepoData struct { 35 | Name string 36 | URL string 37 | MirrorURL string 38 | 39 | Owner struct{ Login string } 40 | LicenseInfo struct{ Name string } 41 | PrimaryLanguage struct{ Name string } 42 | 43 | CreatedAt time.Time 44 | UpdatedAt time.Time 45 | 46 | DefaultBranchRef struct { 47 | Target struct { 48 | Commit struct { // this is the last commit 49 | AuthoredDate time.Time 50 | RecentCommits struct { 51 | TotalCount int 52 | } `graphql:"recentcommits:history(since:$legacyCommitLookback)"` 53 | } `graphql:"... on Commit"` 54 | } 55 | } 56 | 57 | StargazerCount int 58 | HasIssuesEnabled bool 59 | IsArchived bool 60 | IsDisabled bool 61 | IsEmpty bool 62 | IsMirror bool 63 | 64 | Watchers struct{ TotalCount int } 65 | 66 | Tags struct { 67 | TotalCount int 68 | } `graphql:"refs(refPrefix:\"refs/tags/\")"` 69 | } 70 | 71 | func queryBasicRepoData(ctx context.Context, client *graphql.Client, u *url.URL) (*basicRepoData, error) { 72 | // Search based on owner and repo name becaues the `repository` query 73 | // better handles changes in ownership and repository name than the 74 | // `resource` query. 75 | // TODO - consider improving support for scp style urls and urls ending in .git 76 | parts := strings.Split(strings.Trim(u.Path, "/"), "/") 77 | owner := parts[0] 78 | name := parts[1] 79 | s := &struct { 80 | Repository basicRepoData `graphql:"repository(owner: $repositoryOwner, name: $repositoryName)"` 81 | }{} 82 | now := time.Now().UTC() 83 | vars := map[string]any{ 84 | "repositoryOwner": graphql.String(owner), 85 | "repositoryName": graphql.String(name), 86 | "legacyCommitLookback": githubapi.GitTimestamp{Time: now.Add(-legacyCommitLookback)}, 87 | } 88 | if err := client.Query(ctx, s, vars); err != nil { 89 | return nil, err 90 | } 91 | return &s.Repository, nil 92 | } 93 | -------------------------------------------------------------------------------- /internal/collector/github/repo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package github 16 | 17 | import ( 18 | "context" 19 | "net/url" 20 | "time" 21 | 22 | "go.uber.org/zap" 23 | 24 | "github.com/ossf/criticality_score/v2/internal/collector/github/legacy" 25 | "github.com/ossf/criticality_score/v2/internal/githubapi" 26 | ) 27 | 28 | // repo implements the projectrepo.Repo interface for a GitHub repository. 29 | type repo struct { 30 | client *githubapi.Client 31 | origURL *url.URL 32 | logger *zap.Logger 33 | 34 | BasicData *basicRepoData 35 | realURL *url.URL 36 | created time.Time 37 | } 38 | 39 | // URL implements the projectrepo.Repo interface. 40 | func (r *repo) URL() *url.URL { 41 | return r.realURL 42 | } 43 | 44 | func (r *repo) init(ctx context.Context) error { 45 | if r.BasicData != nil { 46 | // Already finished. Don't init() more than once. 47 | return nil 48 | } 49 | r.logger.Debug("Fetching basic data from GitHub") 50 | data, err := queryBasicRepoData(ctx, r.client.GraphQL(), r.origURL) 51 | if err != nil { 52 | return err 53 | } 54 | r.logger.Debug("Fetching created time") 55 | if created, err := legacy.FetchCreatedTime(ctx, r.client, data.Owner.Login, data.Name, data.CreatedAt); err != nil { 56 | return err 57 | } else { 58 | r.created = created 59 | } 60 | r.realURL, err = url.Parse(data.URL) 61 | if err != nil { 62 | return err 63 | } 64 | // Set BasicData last as it is used to indicate init() has been called. 65 | r.BasicData = data 66 | return nil 67 | } 68 | 69 | func (r *repo) owner() string { 70 | return r.BasicData.Owner.Login 71 | } 72 | 73 | func (r *repo) name() string { 74 | return r.BasicData.Name 75 | } 76 | 77 | func (r *repo) updatedAt() time.Time { 78 | return r.BasicData.DefaultBranchRef.Target.Commit.AuthoredDate 79 | } 80 | 81 | func (r *repo) createdAt() time.Time { 82 | return r.created 83 | } 84 | -------------------------------------------------------------------------------- /internal/collector/githubmentions/source.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package githubmentions provides a Collector that returns a Set for the 16 | // number of mentions a given repository has in commit messages as returned by 17 | // GitHub's search interface. 18 | // 19 | // This signal formed the basis of the original version of dependent count, 20 | // however it is a noisy, unreliable signal. 21 | package githubmentions 22 | 23 | import ( 24 | "context" 25 | "fmt" 26 | "net/url" 27 | "strings" 28 | 29 | "github.com/google/go-github/v47/github" 30 | 31 | "github.com/ossf/criticality_score/v2/internal/collector/projectrepo" 32 | "github.com/ossf/criticality_score/v2/internal/collector/signal" 33 | "github.com/ossf/criticality_score/v2/internal/githubapi" 34 | ) 35 | 36 | type mentionSet struct { 37 | MentionCount signal.Field[int] `signal:"github_mention_count,legacy"` 38 | } 39 | 40 | func (s *mentionSet) Namespace() signal.Namespace { 41 | return signal.Namespace("github_mentions") 42 | } 43 | 44 | type Source struct { 45 | client *githubapi.Client 46 | } 47 | 48 | func NewSource(c *githubapi.Client) signal.Source { 49 | return &Source{ 50 | client: c, 51 | } 52 | } 53 | 54 | func (c *Source) EmptySet() signal.Set { 55 | return &mentionSet{} 56 | } 57 | 58 | func (c *Source) IsSupported(r projectrepo.Repo) bool { 59 | return true 60 | } 61 | 62 | func (c *Source) Get(ctx context.Context, r projectrepo.Repo, _ string) (signal.Set, error) { 63 | s := &mentionSet{} 64 | if c, err := c.githubSearchTotalCommitMentions(ctx, r.URL()); err != nil { 65 | return nil, err 66 | } else { 67 | s.MentionCount.Set(c) 68 | } 69 | return s, nil 70 | } 71 | 72 | func (c *Source) githubSearchTotalCommitMentions(ctx context.Context, u *url.URL) (int, error) { 73 | repoName := strings.Trim(u.Path, "/") 74 | opts := &github.SearchOptions{ 75 | ListOptions: github.ListOptions{PerPage: 1}, 76 | } 77 | commits, _, err := c.client.Rest().Search.Commits(ctx, fmt.Sprintf("\"%s\"", repoName), opts) 78 | if err != nil { 79 | return 0, err 80 | } 81 | return commits.GetTotal(), nil 82 | } 83 | -------------------------------------------------------------------------------- /internal/collector/projectrepo/repo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package projectrepo 16 | 17 | import ( 18 | "context" 19 | "net/url" 20 | ) 21 | 22 | // Repo is the core interface representing a project's source repository. 23 | type Repo interface { 24 | URL() *url.URL 25 | } 26 | 27 | // Factory is used to obtain new instances of Repo. 28 | type Factory interface { 29 | // New returns a new instance of Repo for the supplied URL. 30 | // 31 | // If the project can not be found, the error NoRepoFound will be returned. 32 | // 33 | // If the project is not valid for use, or there is any other issue creating 34 | // the Repo, Repo will be nil and an error will be returned. 35 | New(context.Context, *url.URL) (Repo, error) 36 | 37 | // Match returns true if this factory can create a new instance of Repo 38 | // repository for the given repository URL. 39 | Match(*url.URL) bool 40 | } 41 | -------------------------------------------------------------------------------- /internal/collector/projectrepo/resolver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package projectrepo 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "net/url" 22 | ) 23 | 24 | // ErrNoFactoryFound is returned when there is no factory that can be used for 25 | // a given URL. 26 | var ErrNoFactoryFound = errors.New("factory not found for url") 27 | 28 | // ErrNoRepoFound is returned when a factory cannot create a Repo for a given 29 | // URL. 30 | var ErrNoRepoFound = errors.New("repo not found") 31 | 32 | // ErrRepoInaccessible is returned when a the Repo may exist, but is unable to 33 | // access the repository for some reason. 34 | var ErrRepoInaccessible = errors.New("repo inaccessible") 35 | 36 | // Resolver is used to resolve a Repo url against a set of Factory instances 37 | // registered with the resolver. 38 | type Resolver struct { 39 | fs []Factory 40 | } 41 | 42 | func (r *Resolver) findFactory(u *url.URL) Factory { 43 | for _, f := range r.fs { 44 | if f.Match(u) { 45 | return f 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | // Register adds the factory f to the set of factories that can be used for 52 | // resolving a url to a Repo. 53 | func (r *Resolver) Register(f Factory) { 54 | r.fs = append(r.fs, f) 55 | } 56 | 57 | // Resolve takes a url u and returns a corresponding instance of Repo if a 58 | // matching factory has been registered. 59 | // 60 | // If a matching factory can not be found an ErrorNotFound will be returned. 61 | // 62 | // The factory may also return an error. 63 | func (r *Resolver) Resolve(ctx context.Context, u *url.URL) (Repo, error) { 64 | f := r.findFactory(u) 65 | if f == nil { 66 | return nil, fmt.Errorf("%w: %s", ErrNoFactoryFound, u) 67 | } 68 | return f.New(ctx, u) 69 | } 70 | -------------------------------------------------------------------------------- /internal/collector/signal/issues.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package signal 16 | 17 | type IssuesSet struct { 18 | UpdatedCount Field[int] `signal:"updated_issues_count,legacy"` 19 | ClosedCount Field[int] `signal:"closed_issues_count,legacy"` 20 | CommentFrequency Field[float64] `signal:"issue_comment_frequency,legacy"` 21 | } 22 | 23 | func (r *IssuesSet) Namespace() Namespace { 24 | return NamespaceIssues 25 | } 26 | -------------------------------------------------------------------------------- /internal/collector/signal/repo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package signal 16 | 17 | import "time" 18 | 19 | //nolint:govet 20 | type RepoSet struct { 21 | URL Field[string] 22 | Language Field[string] 23 | License Field[string] 24 | 25 | StarCount Field[int] 26 | CreatedAt Field[time.Time] 27 | UpdatedAt Field[time.Time] 28 | 29 | CreatedSince Field[int] `signal:"legacy"` 30 | UpdatedSince Field[int] `signal:"legacy"` 31 | 32 | ContributorCount Field[int] `signal:"legacy"` 33 | OrgCount Field[int] `signal:"legacy"` 34 | 35 | CommitFrequency Field[float64] `signal:"legacy"` 36 | RecentReleaseCount Field[int] `signal:"legacy"` 37 | } 38 | 39 | func (r *RepoSet) Namespace() Namespace { 40 | return NamespaceRepo 41 | } 42 | -------------------------------------------------------------------------------- /internal/collector/signal/source.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package signal 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/ossf/criticality_score/v2/internal/collector/projectrepo" 21 | ) 22 | 23 | // A Source is used to get a set of signals for a given project repository. 24 | type Source interface { 25 | // EmptySet returns an empty instance of a signal Set that can be used for 26 | // determining the namespace and signals supported by the Source. 27 | EmptySet() Set 28 | 29 | // IsSupported returns true if the Source supports the supplied Repo. 30 | IsSupported(projectrepo.Repo) bool 31 | 32 | // Get gathers and returns a Set of signals for the given project repo r. 33 | // 34 | // An optional string jobID can be specified and may be used by the Source 35 | // to manage caches related to a collection run. 36 | // 37 | // An error is returned if it is unable to successfully gather the signals, 38 | // or if the context is cancelled. 39 | Get(ctx context.Context, r projectrepo.Repo, jobID string) (Set, error) 40 | } 41 | -------------------------------------------------------------------------------- /internal/envflag/envflag.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package envflag is a simple library for associating environment variables with flags. 16 | // 17 | // If using the default flag.CommandLine FlagSet, just call envflag.Parse() instead of flag.Parse(). 18 | // 19 | // Assign environment variables to flags using the Map type: 20 | // 21 | // var m := envflag.Map{ 22 | // "MY_ENV_VAR": "my-flag" 23 | // } 24 | // 25 | // If the flag and the environment variable is set the flag takes precidence. 26 | package envflag 27 | 28 | import ( 29 | "errors" 30 | "flag" 31 | "os" 32 | ) 33 | 34 | type Map map[string]string 35 | 36 | func (m Map) Assign(fs *flag.FlagSet) error { 37 | for env, f := range m { 38 | if v, ok := os.LookupEnv(env); ok && v != "" { 39 | err := fs.Set(f, v) 40 | if err != nil { 41 | return err 42 | } 43 | } 44 | } 45 | return nil 46 | } 47 | 48 | func ParseFlagSet(fs *flag.FlagSet, args []string, m Map) error { 49 | err := m.Assign(fs) 50 | if err != nil { 51 | switch fs.ErrorHandling() { 52 | case flag.ContinueOnError: 53 | return err 54 | case flag.ExitOnError: 55 | if errors.Is(err, flag.ErrHelp) { 56 | os.Exit(0) 57 | } 58 | os.Exit(2) 59 | case flag.PanicOnError: 60 | panic(err) 61 | } 62 | } 63 | return fs.Parse(args) 64 | } 65 | 66 | func Parse(m Map) { 67 | if err := m.Assign(flag.CommandLine); err != nil { 68 | // flag.CommandLine is set for ExitOnError 69 | if errors.Is(err, flag.ErrHelp) { 70 | os.Exit(0) 71 | } 72 | os.Exit(2) 73 | } 74 | flag.Parse() 75 | } 76 | -------------------------------------------------------------------------------- /internal/githubapi/batch.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package githubapi 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | 22 | "github.com/hasura/go-graphql-client" 23 | ) 24 | 25 | func constructBatchQuery[T any](queries map[string]string) (string, map[string]string, error) { 26 | var t T 27 | 28 | // Serializes type T into GraphQL format. The result is used to build up the 29 | // batch query. 30 | queryObj, err := graphql.ConstructQuery(t, map[string]any{}) 31 | if err != nil { 32 | return "", nil, err 33 | } 34 | fieldMap := make(map[string]string) 35 | query := "" 36 | idx := 0 37 | for key, subquery := range queries { 38 | // Generate a field name to track which result belongs to which query. 39 | name := fmt.Sprintf("field%d", idx) 40 | fieldMap[key] = name 41 | 42 | // Join the subquery to the current set of queries, combining it with 43 | // the field name, and the queryObj generated for T. 44 | // The value being appended looks like: 'field0:search(foo="bah"){blah}' 45 | query = query + fmt.Sprintf("%s:%s%s", name, subquery, queryObj) 46 | idx++ 47 | } 48 | return "{" + query + "}", fieldMap, nil 49 | } 50 | 51 | // BatchQuery can be used to batch a set of requests together to GitHub's 52 | // GraphQL API. 53 | // 54 | // The queries must be for objects represented by type T. T should be a struct 55 | // to work correctly. 56 | func BatchQuery[T any](ctx context.Context, c *Client, queries map[string]string) (map[string]T, error) { 57 | // TODO: an upper bound should be added 58 | if len(queries) == 0 { 59 | // TODO: consider just returning an empty result set rather than panicing. 60 | panic("no query to run") 61 | } 62 | 63 | // Generate the query from the type T and the set of queries. 64 | query, fieldMap, err := constructBatchQuery[T](queries) 65 | if err != nil { 66 | return nil, fmt.Errorf("failed to construct batch query: %w", err) 67 | } 68 | 69 | // Execute the raw query. 70 | data, err := c.GraphQL().ExecRaw(ctx, query, map[string]any{}) 71 | if err != nil { 72 | return nil, fmt.Errorf("failed executing raw query '%s': %w", query, err) 73 | } 74 | 75 | // Parse the JSON response into a map where the keys are the field names 76 | // and the values the results we looked up as type T. 77 | var resultByField map[string]T 78 | if err := json.Unmarshal(data, &resultByField); err != nil { 79 | return nil, fmt.Errorf("json parsing failed: %w", err) 80 | } 81 | 82 | // Remap the results so the keys supplied in the queries argument are mapped 83 | // to their results. 84 | res := map[string]T{} 85 | for key, name := range fieldMap { 86 | res[key] = resultByField[name] 87 | } 88 | return res, nil 89 | } 90 | -------------------------------------------------------------------------------- /internal/githubapi/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package githubapi 16 | 17 | import ( 18 | "net/http" 19 | 20 | "github.com/google/go-github/v47/github" 21 | "github.com/hasura/go-graphql-client" 22 | ) 23 | 24 | // Client provides simple access to GitHub's REST and GraphQL APIs. 25 | type Client struct { 26 | restClient *github.Client 27 | graphClient *graphql.Client 28 | } 29 | 30 | // NewClient creates a new instances of Client. 31 | func NewClient(client *http.Client) *Client { 32 | // Wrap the Transport for the GraphQL client to produce more useful errors. 33 | graphClient := *client // deref to copy the struct 34 | graphClient.Transport = &graphQLRoundTripper{inner: client.Transport} 35 | 36 | return &Client{ 37 | restClient: github.NewClient(client), 38 | graphClient: graphql.NewClient(DefaultGraphQLEndpoint, &graphClient), 39 | } 40 | } 41 | 42 | // Rest returns a client for communicating with GitHub's REST API. 43 | func (c *Client) Rest() *github.Client { 44 | return c.restClient 45 | } 46 | 47 | // GraphQL returns a client for communicating with GitHub's GraphQL API. 48 | func (c *Client) GraphQL() *graphql.Client { 49 | return c.graphClient 50 | } 51 | -------------------------------------------------------------------------------- /internal/githubapi/graphql.go: -------------------------------------------------------------------------------- 1 | package githubapi 2 | 3 | import "time" 4 | 5 | // DefaultGraphQLEndpoint is the default URL for the GitHub GraphQL API. 6 | const DefaultGraphQLEndpoint = "https://api.github.com/graphql" 7 | 8 | // GitTimestamp is an ISO-8601 encoded date for use with the GitHub GraphQL API. 9 | // Unlike the DateTime type, GitTimestamp is not converted in UTC. 10 | 11 | type GitTimestamp struct{ time.Time } 12 | -------------------------------------------------------------------------------- /internal/githubapi/pagination/pagination.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pagination 16 | 17 | import ( 18 | "context" 19 | "io" 20 | 21 | "github.com/hasura/go-graphql-client" 22 | ) 23 | 24 | // PagedQuery implementors go from being regular query struct passed to githubv4.Query() 25 | // to a query that can be paginated. 26 | type PagedQuery interface { 27 | Total() int 28 | Length() int 29 | Get(int) any 30 | Reset() 31 | HasNextPage() bool 32 | NextPageVars() map[string]any 33 | } 34 | 35 | type Cursor struct { 36 | ctx context.Context 37 | client *graphql.Client 38 | query PagedQuery 39 | vars map[string]any 40 | cur int 41 | } 42 | 43 | func Query(ctx context.Context, client *graphql.Client, query PagedQuery, vars map[string]any) (*Cursor, error) { 44 | c := &Cursor{ 45 | ctx: ctx, 46 | client: client, 47 | query: query, 48 | vars: vars, 49 | } 50 | if err := c.queryNextPage(); err != nil { 51 | return nil, err 52 | } 53 | return c, nil 54 | } 55 | 56 | func (c *Cursor) queryNextPage() error { 57 | // Merge the next page vars with the current vars 58 | newVars := c.query.NextPageVars() 59 | for k, v := range newVars { 60 | c.vars[k] = v 61 | } 62 | // Reset the current position 63 | c.cur = 0 64 | // ZERO the query... 65 | c.query.Reset() 66 | // Execute the query 67 | return c.client.Query(c.ctx, c.query, c.vars) 68 | } 69 | 70 | func (c *Cursor) atEndOfPage() bool { 71 | return c.cur >= c.query.Length() 72 | } 73 | 74 | func (c *Cursor) isLastPage() bool { 75 | return !c.query.HasNextPage() 76 | } 77 | 78 | func (c *Cursor) Total() int { 79 | return c.query.Total() 80 | } 81 | 82 | func (c *Cursor) Next() (any, error) { 83 | if c.atEndOfPage() { 84 | // There are no more nodes in this page, so we need another page. 85 | if c.isLastPage() { 86 | // There are no more pages, so return an EOF 87 | return nil, io.EOF 88 | } 89 | // Grab the next page. 90 | if err := c.queryNextPage(); err != nil { 91 | return nil, err 92 | } 93 | if c.atEndOfPage() { 94 | // Despite grabing a new page we have no results, 95 | // so return an EOF. 96 | return nil, io.EOF 97 | } 98 | } 99 | val := c.query.Get(c.cur) 100 | c.cur++ 101 | return val, nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/infile/infile.go: -------------------------------------------------------------------------------- 1 | package infile 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // fileOpenFunc makes it possible to mock os.Open() for testing. 10 | type fileOpenFunc func(string) (*os.File, error) 11 | 12 | var ( 13 | fileOpen fileOpenFunc = os.Open 14 | 15 | // The name that is used to represent stdin. 16 | StdinName = "-" 17 | ) 18 | 19 | // Open opens and returns a file for input with the given filename. 20 | // 21 | // If filename is equal to o.StdoutName, os.Stdin will be used. 22 | // If filename does not exist, an error will be returned. 23 | // If filename does exist, the file will be opened and returned. 24 | func Open(ctx context.Context, filename string) (io.ReadCloser, error) { 25 | if StdinName != "" && filename == StdinName { 26 | return os.Stdin, nil 27 | } 28 | return fileOpen(filename) 29 | } 30 | -------------------------------------------------------------------------------- /internal/infile/infile_test.go: -------------------------------------------------------------------------------- 1 | package infile 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestOpenStdin(t *testing.T) { 11 | origStdinName := StdinName 12 | defer func() { StdinName = origStdinName }() 13 | StdinName = "-stdin-" 14 | f, err := Open(context.Background(), "-stdin-") 15 | if err != nil { 16 | t.Fatalf("Open() == %v, want nil", err) 17 | } 18 | if f != os.Stdin { 19 | t.Fatal("Open() == not stdin, want stdin") 20 | } 21 | } 22 | 23 | func TestOpen(t *testing.T) { 24 | want := "path/to/file" 25 | got := "" 26 | fileOpen = func(filename string) (*os.File, error) { 27 | got = filename 28 | return &os.File{}, nil 29 | } 30 | 31 | f, err := Open(context.Background(), want) 32 | if err != nil { 33 | t.Fatalf("Open() == %v, want nil", err) 34 | } 35 | if f == nil { 36 | t.Fatal("Open() == nil, want a file") 37 | } 38 | if got != want { 39 | t.Fatalf("Open(%q) opened %q", want, got) 40 | } 41 | } 42 | 43 | func TestOpenError(t *testing.T) { 44 | want := errors.New("test error") 45 | fileOpen = func(filename string) (*os.File, error) { 46 | return nil, want 47 | } 48 | _, err := Open(context.Background(), "path/to/file") 49 | if err == nil { 50 | t.Fatalf("Open() is nil, want %v", want) 51 | } 52 | if !errors.Is(err, want) { 53 | t.Fatalf("Open() returned %v, want %v", err, want) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/iterator/batch.go: -------------------------------------------------------------------------------- 1 | package iterator 2 | 3 | import "fmt" 4 | 5 | type batchIter[T any] struct { 6 | input IterCloser[T] 7 | lastErr error 8 | item []T 9 | batchSize int 10 | } 11 | 12 | func (i *batchIter[T]) nextBatch() ([]T, error) { 13 | var batch []T 14 | 15 | for i.input.Next() { 16 | item := i.input.Item() 17 | batch = append(batch, item) 18 | if len(batch) >= i.batchSize { 19 | break 20 | } 21 | } 22 | if err := i.input.Err(); err != nil { 23 | // The input iterator failed, so return an error. 24 | return nil, fmt.Errorf("input iter: %w", err) 25 | } 26 | if len(batch) == 0 { 27 | // We've passed the end. 28 | return nil, nil 29 | } 30 | return batch, nil 31 | } 32 | 33 | func (i *batchIter[T]) Item() []T { 34 | return i.item 35 | } 36 | 37 | func (i *batchIter[T]) Next() bool { 38 | if i.lastErr != nil { 39 | // Stop if we've encountered an error. 40 | return false 41 | } 42 | batch, err := i.nextBatch() 43 | if err != nil { 44 | i.lastErr = err 45 | return false 46 | } 47 | if len(batch) == 0 { 48 | // We are also done at this point. 49 | return false 50 | } 51 | i.item = batch 52 | return true 53 | } 54 | 55 | func (i *batchIter[T]) Err() error { 56 | return i.lastErr 57 | } 58 | 59 | func (i *batchIter[T]) Close() error { 60 | if err := i.input.Close(); err != nil { 61 | return fmt.Errorf("input close: %w", i.input.Close()) 62 | } 63 | return nil 64 | } 65 | 66 | func Batch[T any](input IterCloser[T], batchSize int) IterCloser[[]T] { 67 | return &batchIter[T]{ 68 | input: input, 69 | batchSize: batchSize, 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/iterator/iterator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package iterator 16 | 17 | import ( 18 | "io" 19 | ) 20 | 21 | // Iter is a simple interface for iterating across a list of items. 22 | // 23 | // This interface is modeled on the bufio.Scanner behavior. 24 | type Iter[T any] interface { 25 | // Item returns the current item in the iterator. 26 | // 27 | // Next() must be called before calling Item(). 28 | Item() T 29 | 30 | // Next advances the iterator to the next item and returns true if there is 31 | // an item to consume, and false if the end of the input has been reached, 32 | // or there has been an error. 33 | // 34 | // Next must be called before each call to Item. 35 | Next() bool 36 | 37 | // Err returns any error produced while iterating. 38 | Err() error 39 | } 40 | 41 | // IterCloser is an iter, but also embeds the io.Closer interface, so it can be 42 | // used to wrap a file for iterating through. 43 | type IterCloser[T any] interface { 44 | Iter[T] 45 | io.Closer 46 | } 47 | -------------------------------------------------------------------------------- /internal/iterator/scanner.go: -------------------------------------------------------------------------------- 1 | package iterator 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // scannerIter implements Iter[string] using a bufio.Scanner to iterate through 10 | // lines in a file. 11 | type scannerIter struct { 12 | c io.Closer 13 | scanner *bufio.Scanner 14 | } 15 | 16 | func (i *scannerIter) Item() string { 17 | return i.scanner.Text() 18 | } 19 | 20 | func (i *scannerIter) Next() bool { 21 | return i.scanner.Scan() 22 | } 23 | 24 | func (i *scannerIter) Err() error { 25 | if err := i.scanner.Err(); err != nil { 26 | return fmt.Errorf("scanner: %w", i.scanner.Err()) 27 | } 28 | return nil 29 | } 30 | 31 | func (i *scannerIter) Close() error { 32 | if err := i.c.Close(); err != nil { 33 | return fmt.Errorf("closer: %w", i.c.Close()) 34 | } 35 | return nil 36 | } 37 | 38 | func Lines(r io.ReadCloser) IterCloser[string] { 39 | return &scannerIter{ 40 | c: r, 41 | scanner: bufio.NewScanner(r), 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/iterator/scanner_test.go: -------------------------------------------------------------------------------- 1 | package iterator_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "slices" 8 | "strings" 9 | "testing" 10 | "testing/iotest" 11 | 12 | "github.com/ossf/criticality_score/v2/internal/iterator" 13 | ) 14 | 15 | func TestScannerIter_Empty(t *testing.T) { 16 | var b bytes.Buffer 17 | i := iterator.Lines(io.NopCloser(&b)) 18 | 19 | if got := i.Next(); got { 20 | t.Errorf("Next() = %v; want false", got) 21 | } 22 | if err := i.Err(); err != nil { 23 | t.Errorf("Err() = %v; want no error", err) 24 | } 25 | } 26 | 27 | func TestScannerIter_SingleLine(t *testing.T) { 28 | want := "test line" 29 | b := bytes.NewBuffer([]byte(want)) 30 | i := iterator.Lines(io.NopCloser(b)) 31 | 32 | if got := i.Next(); !got { 33 | t.Errorf("Next() = %v; want true", got) 34 | } 35 | if err := i.Err(); err != nil { 36 | t.Errorf("Err() = %v; want no error", err) 37 | } 38 | if got := i.Item(); got != want { 39 | t.Errorf("Item() = %v; want %v", got, want) 40 | } 41 | if got := i.Next(); got { 42 | t.Errorf("Next()#2 = %v; want false", got) 43 | } 44 | if err := i.Err(); err != nil { 45 | t.Errorf("Err()#2 = %v; want no error", err) 46 | } 47 | } 48 | 49 | func TestScannerIter_MultiLine(t *testing.T) { 50 | want := []string{"line one", "line two", "line three"} 51 | b := bytes.NewBuffer([]byte(strings.Join(want, "\n"))) 52 | i := iterator.Lines(io.NopCloser(b)) 53 | 54 | var got []string 55 | for i.Next() { 56 | item := i.Item() 57 | got = append(got, item) 58 | } 59 | if err := i.Err(); err != nil { 60 | t.Errorf("Err() = %v; want no error", err) 61 | } 62 | if !slices.Equal(got, want) { 63 | t.Errorf("Iterator returned %v, want %v", got, want) 64 | } 65 | } 66 | 67 | func TestScannerIter_Error(t *testing.T) { 68 | want := errors.New("error") 69 | r := iotest.ErrReader(want) 70 | i := iterator.Lines(io.NopCloser(r)) 71 | 72 | if got := i.Next(); got { 73 | t.Errorf("Next() = %v; want false", got) 74 | } 75 | if err := i.Err(); err == nil || !errors.Is(err, want) { 76 | t.Errorf("Err() = %v; want %v", err, want) 77 | } 78 | } 79 | 80 | type closerFn func() error 81 | 82 | func (c closerFn) Close() error { 83 | return c() 84 | } 85 | 86 | func TestScannerIter_Close(t *testing.T) { 87 | got := 0 88 | i := iterator.Lines(&struct { 89 | closerFn 90 | io.Reader 91 | }{ 92 | closerFn: closerFn(func() error { 93 | got++ 94 | return nil 95 | }), 96 | Reader: &bytes.Buffer{}, 97 | }) 98 | err := i.Close() 99 | 100 | if got != 1 { 101 | t.Errorf("Close() called %d times; want 1", got) 102 | } 103 | if err != nil { 104 | t.Errorf("Err() = %v; want no error", err) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /internal/iterator/slice.go: -------------------------------------------------------------------------------- 1 | package iterator 2 | 3 | // sliceIter implements iter using a slice for iterating. 4 | type sliceIter[T any] struct { 5 | values []T 6 | next int 7 | } 8 | 9 | func (i *sliceIter[T]) Item() T { 10 | return i.values[i.next-1] 11 | } 12 | 13 | func (i *sliceIter[T]) Next() bool { 14 | if i.next <= len(i.values) { 15 | i.next++ 16 | } 17 | return i.next <= len(i.values) 18 | } 19 | 20 | func (i *sliceIter[T]) Err() error { 21 | return nil 22 | } 23 | 24 | func (i *sliceIter[T]) Close() error { 25 | return nil 26 | } 27 | 28 | func Slice[T any](slice []T) IterCloser[T] { 29 | return &sliceIter[T]{ 30 | values: slice, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/iterator/slice_test.go: -------------------------------------------------------------------------------- 1 | package iterator_test 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | 7 | "github.com/ossf/criticality_score/v2/internal/iterator" 8 | ) 9 | 10 | func TestSliceIter_Empty(t *testing.T) { 11 | i := iterator.Slice[int]([]int{}) 12 | 13 | if got := i.Next(); got { 14 | t.Errorf("Next() = %v; want false", got) 15 | } 16 | } 17 | 18 | func TestSliceIter_SingleEntry(t *testing.T) { 19 | want := 42 20 | i := iterator.Slice[int]([]int{want}) 21 | 22 | if got := i.Next(); !got { 23 | t.Errorf("Next() = %v; want true", got) 24 | } 25 | if got := i.Item(); got != want { 26 | t.Errorf("Item() = %v; want %v", got, want) 27 | } 28 | if got := i.Next(); got { 29 | t.Errorf("Next()#2 = %v; want false", got) 30 | } 31 | } 32 | 33 | func TestSliceIter_MultiEntry(t *testing.T) { 34 | want := []int{1, 2, 3, 42, 1337} 35 | i := iterator.Slice[int](want) 36 | 37 | var got []int 38 | for i.Next() { 39 | got = append(got, i.Item()) 40 | } 41 | 42 | if !slices.Equal(got, want) { 43 | t.Errorf("Iterator returned %v, want %v", got, want) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/log/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package log 16 | 17 | import ( 18 | "github.com/blendle/zapdriver" 19 | "go.uber.org/zap" 20 | "go.uber.org/zap/zapcore" 21 | ) 22 | 23 | const ( 24 | configLogEnvKey = "log-env" 25 | configLogLevelKey = "log-level" 26 | ) 27 | 28 | func dev() (zap.Config, []zap.Option) { 29 | c := zap.NewDevelopmentConfig() 30 | c.EncoderConfig.CallerKey = zapcore.OmitKey 31 | // TODO, use go-isatty to choose color VS no-color 32 | c.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder 33 | c.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05.000") 34 | return c, []zap.Option{} 35 | } 36 | 37 | func gcp() (zap.Config, []zap.Option) { 38 | c := zapdriver.NewProductionConfig() 39 | // Make sure sampling is disabled. 40 | c.Sampling = nil 41 | // Build the logger and ensure we use the zapdriver Core so that labels 42 | // are handled correctly. 43 | return c, []zap.Option{zapdriver.WrapCore()} 44 | } 45 | 46 | // NewLogger returns a new instance of the zap.Logger based on the specified 47 | // env and level. 48 | // 49 | // The level sets the minimum level log messages will be output, with 50 | // env being used to configure the logger for a particular environment. 51 | func NewLogger(e Env, l zapcore.Level) (*zap.Logger, error) { 52 | var c zap.Config 53 | var opts []zap.Option 54 | switch e { 55 | case GCPEnv: 56 | c, opts = gcp() 57 | default: 58 | c, opts = dev() 59 | } 60 | 61 | c.Level = zap.NewAtomicLevelAt(l) 62 | return c.Build(opts...) 63 | } 64 | 65 | // NewLoggerFromConfigMap returns a new instance of the zap.Logger based on 66 | // the value of the keys "log-env" and "log-level" in the config map. 67 | // 68 | // If the "log-env" key is not present, defaultEnv will be used. 69 | // If the "log-level" key is not present, defaultLevel will be used. 70 | func NewLoggerFromConfigMap(defaultEnv Env, defaultLevel zapcore.Level, config map[string]string) (*zap.Logger, error) { 71 | // Extract the log environment from the config, if it exists. 72 | logEnv := defaultEnv 73 | if val := config[configLogEnvKey]; val != "" { 74 | if err := logEnv.UnmarshalText([]byte(val)); err != nil { 75 | return nil, err 76 | } 77 | } 78 | 79 | // Extract the log level from the config, if it exists. 80 | logLevel := defaultLevel 81 | if val := config[configLogLevelKey]; val != "" { 82 | if err := logLevel.Set(val); err != nil { 83 | return nil, err 84 | } 85 | } 86 | 87 | return NewLogger(logEnv, logLevel) 88 | } 89 | -------------------------------------------------------------------------------- /internal/log/env.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package log 16 | 17 | import "errors" 18 | 19 | type Env int 20 | 21 | const ( 22 | UnknownEnv = Env(0) 23 | DevEnv = Env(iota) 24 | GCPEnv 25 | 26 | DefaultEnv = DevEnv 27 | ) 28 | 29 | var ErrorUnkownEnv = errors.New("unknown logging environment") 30 | 31 | // LookupEnv will return the instance of Env that corresponds to text. 32 | // 33 | // If text does not match a known environment UnknownEnv will be returned. 34 | func LookupEnv(text string) Env { 35 | switch text { 36 | case "dev": 37 | return DevEnv 38 | case "gcp": 39 | return GCPEnv 40 | default: 41 | return UnknownEnv 42 | } 43 | } 44 | 45 | func (e Env) String() string { 46 | switch e { 47 | case DevEnv: 48 | return "dev" 49 | case GCPEnv: 50 | return "gcp" 51 | default: 52 | // panic? 53 | return "unknown" 54 | } 55 | } 56 | 57 | // UnmarshalText implements the encoding.TextUnmarshaler interface. 58 | func (e *Env) UnmarshalText(text []byte) error { 59 | *e = LookupEnv(string(text)) 60 | if *e == UnknownEnv { 61 | return ErrorUnkownEnv 62 | } 63 | return nil 64 | } 65 | 66 | func (e Env) MarshalText() ([]byte, error) { 67 | return []byte(e.String()), nil 68 | } 69 | -------------------------------------------------------------------------------- /internal/log/env_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package log 16 | 17 | import ( 18 | "bytes" 19 | "math" 20 | "testing" 21 | ) 22 | 23 | func TestLookupEnv(t *testing.T) { 24 | tests := []struct { 25 | name string 26 | text string 27 | want Env 28 | }{ 29 | {"dev", "dev", DevEnv}, 30 | {"gcp", "gcp", GCPEnv}, 31 | {"unknown", "unknown", UnknownEnv}, 32 | {"empty", "", UnknownEnv}, 33 | } 34 | for _, test := range tests { 35 | t.Run(test.name, func(t *testing.T) { 36 | if got := LookupEnv(test.text); got != test.want { 37 | t.Errorf("LookupEnv() = %v, want %v", got, test.want) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func TestEnv_String(t *testing.T) { 44 | tests := []struct { //nolint:govet 45 | name string 46 | e Env 47 | want string 48 | }{ 49 | {"dev", DevEnv, "dev"}, 50 | {"gcp", GCPEnv, "gcp"}, 51 | {"unknown", UnknownEnv, "unknown"}, 52 | } 53 | for _, test := range tests { 54 | t.Run(test.name, func(t *testing.T) { 55 | if got := test.e.String(); got != test.want { 56 | t.Errorf("String() = %v, want %v", got, test.want) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | func TestEnv_UnmarshalText(t *testing.T) { 63 | tests := []struct { //nolint:govet 64 | name string 65 | e Env 66 | text []byte 67 | value string 68 | wantErr bool 69 | }{ 70 | { 71 | name: "unknown", 72 | text: []byte("unknown"), 73 | value: "unknown", 74 | wantErr: true, 75 | }, 76 | { 77 | name: "dev", 78 | text: []byte("dev"), 79 | value: "dev", 80 | }, 81 | } 82 | for _, test := range tests { 83 | t.Run(test.name, func(t *testing.T) { 84 | if err := test.e.UnmarshalText(test.text); (err != nil) != test.wantErr || test.e.String() != test.value { 85 | t.Errorf("UnmarshalText() error = %v, wantErr %v", err, test.wantErr) 86 | } 87 | }) 88 | } 89 | } 90 | 91 | func TestEnv_MarshalText(t *testing.T) { 92 | tests := []struct { //nolint:govet 93 | name string 94 | e Env 95 | want []byte 96 | }{ 97 | {"dev", DevEnv, []byte("dev")}, 98 | {"unknown", UnknownEnv, []byte("unknown")}, 99 | {"empty", math.MaxInt, []byte("unknown")}, 100 | } 101 | for _, test := range tests { 102 | t.Run(test.name, func(t *testing.T) { 103 | got, err := test.e.MarshalText() 104 | 105 | if !bytes.Equal(got, test.want) || err != nil { 106 | // this function never returns an error so err should always be nil 107 | t.Errorf("MarshalText() got = %v, want %v", got, test.want) 108 | } 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /internal/scorer/algorithm/algorithm.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package algorithm 16 | 17 | type Algorithm interface { 18 | Score(record map[string]float64) float64 19 | } 20 | 21 | type Factory func(inputs []*Input) (Algorithm, error) 22 | -------------------------------------------------------------------------------- /internal/scorer/algorithm/distribution.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package algorithm 16 | 17 | import ( 18 | "math" 19 | ) 20 | 21 | type Distribution struct { 22 | normalizeFn func(float64) float64 23 | name string 24 | } 25 | 26 | func (d *Distribution) String() string { 27 | return d.name 28 | } 29 | 30 | func (d *Distribution) Normalize(v float64) float64 { 31 | return d.normalizeFn(v) 32 | } 33 | 34 | var ( 35 | normalizationFuncs = map[string]func(float64) float64{ 36 | "linear": func(v float64) float64 { return v }, 37 | "zipfian": func(v float64) float64 { return math.Log(1 + v) }, 38 | } 39 | DefaultDistributionName = "linear" 40 | ) 41 | 42 | func LookupDistribution(name string) *Distribution { 43 | fn, ok := normalizationFuncs[name] 44 | if !ok { 45 | return nil 46 | } 47 | return &Distribution{ 48 | name: name, 49 | normalizeFn: fn, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/scorer/algorithm/input.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package algorithm 16 | 17 | type Bounds struct { 18 | Lower float64 `yaml:"lower"` 19 | Upper float64 `yaml:"upper"` 20 | SmallerIsBetter bool `yaml:"smaller_is_better"` 21 | } 22 | 23 | func (b Bounds) Apply(v float64) float64 { 24 | // |----L---v----U----| == v stays as is 25 | // |--v-L--------U----| == v moves to L 26 | // |----L--------U--v-| == v moves to U 27 | if v < b.Lower { 28 | v = b.Lower 29 | } else if v > b.Upper { 30 | v = b.Upper 31 | } 32 | // Afterwards we move L to 0, by calculating v = v - L 33 | v = v - b.Lower 34 | if b.SmallerIsBetter { 35 | // If "SmallerIsBetter" is true then invert the value with the 36 | // threshold. So, a 0 value becomes the threshold value and a 37 | // value at the threshold becomes 0. 38 | // TODO: consider how this affects the distribution 39 | v = b.Threshold() - v 40 | } 41 | return v 42 | } 43 | 44 | func (b Bounds) Threshold() float64 { 45 | return b.Upper - b.Lower 46 | } 47 | 48 | type Input struct { 49 | Source Value 50 | Bounds *Bounds 51 | Distribution *Distribution 52 | Tags []string 53 | Weight float64 54 | } 55 | 56 | func (i *Input) Value(fields map[string]float64) (float64, bool) { 57 | v, ok := i.Source.Value(fields) 58 | if !ok { 59 | return 0, false 60 | } 61 | var den float64 = 1 62 | if i.Bounds != nil { 63 | v = i.Bounds.Apply(v) 64 | den = i.Distribution.Normalize(i.Bounds.Threshold()) 65 | } 66 | return i.Distribution.Normalize(v) / den, true 67 | } 68 | -------------------------------------------------------------------------------- /internal/scorer/algorithm/registry.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package algorithm 16 | 17 | import "fmt" 18 | 19 | // Registry is used to map a name to a Factory that creates Algorithm instances 20 | // for the given name. 21 | type Registry struct { 22 | as map[string]Factory 23 | } 24 | 25 | // NewRegistry returns a new Registry instance. 26 | func NewRegistry() *Registry { 27 | return &Registry{ 28 | as: make(map[string]Factory), 29 | } 30 | } 31 | 32 | // Register adds the Factory for the corresponding Key to the registry. 33 | // 34 | // If another Factory has been registered with the same Key it will be 35 | // replaced. 36 | func (r *Registry) Register(name string, f Factory) { 37 | r.as[name] = f 38 | } 39 | 40 | // NewAlgorithm generates a new instance of Algorithm for the supplied name and 41 | // fields. 42 | // 43 | // If the registry does not have a Factory for the supplied name an error will 44 | // be returned. 45 | // 46 | // If the Algorithm fails to be created by the Factory, an error will also be 47 | // returned and the Algorithm will be nil. 48 | func (r *Registry) NewAlgorithm(name string, inputs []*Input) (Algorithm, error) { 49 | f, ok := r.as[name] 50 | if !ok { 51 | return nil, fmt.Errorf("unknown algorithm %s", name) 52 | } 53 | return f(inputs) 54 | } 55 | -------------------------------------------------------------------------------- /internal/scorer/algorithm/value.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package algorithm 16 | 17 | type Value interface { 18 | // Value takes in a set of fields does some work and returns either the 19 | // result and true to indicate success, or 0 and false to indicate 20 | // the result could not be generated. 21 | Value(fields map[string]float64) (float64, bool) 22 | } 23 | 24 | // Field implements the Value interface, but simply returns the raw value of 25 | // the named field. 26 | type Field string 27 | 28 | func (f Field) String() string { 29 | return string(f) 30 | } 31 | 32 | // Value implements the Value interface. 33 | func (f Field) Value(fields map[string]float64) (float64, bool) { 34 | v, ok := fields[string(f)] 35 | return v, ok 36 | } 37 | 38 | type Condition func(fields map[string]float64) bool 39 | 40 | func NotCondition(c Condition) Condition { 41 | return func(fields map[string]float64) bool { 42 | return !c(fields) 43 | } 44 | } 45 | 46 | func ExistsCondition(f Field) Condition { 47 | return func(fields map[string]float64) bool { 48 | _, exists := fields[f.String()] 49 | return exists 50 | } 51 | } 52 | 53 | // ConditionalValue wraps an Inner value that will only be returned if the 54 | // Condition returns true. 55 | type ConditionalValue struct { 56 | Condition Condition 57 | Inner Value 58 | } 59 | 60 | // Value implements the Value interface. 61 | func (cv *ConditionalValue) Value(fields map[string]float64) (float64, bool) { 62 | v, ok := cv.Inner.Value(fields) 63 | if !ok { 64 | return 0, false 65 | } 66 | if cv.Condition(fields) { 67 | return v, true 68 | } else { 69 | return 0, false 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/scorer/algorithm/value_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package algorithm 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestCondition(t *testing.T) { 22 | tests := []struct { //nolint:govet 23 | name string 24 | f Field 25 | fields map[string]float64 26 | existsWant bool // for ExistsCondition() 27 | notWant bool // for NotCondition() 28 | }{ 29 | { 30 | name: "exists", 31 | f: Field("a"), 32 | fields: map[string]float64{"a": 1}, 33 | existsWant: true, 34 | notWant: false, 35 | }, 36 | { 37 | name: "not exists", 38 | f: Field("a"), 39 | fields: map[string]float64{"b": 1}, 40 | existsWant: false, 41 | notWant: true, 42 | }, 43 | } 44 | for _, test := range tests { 45 | t.Run(test.name, func(t *testing.T) { 46 | got := ExistsCondition(test.f) 47 | 48 | if got(test.fields) != test.existsWant { 49 | t.Errorf("ExistsCondition() = %v, wantVal %v", got(test.fields), test.existsWant) 50 | } 51 | 52 | got = NotCondition(got) 53 | 54 | if got(test.fields) != test.notWant { 55 | t.Errorf("NotCondition() = %v, wantVal %v", got(test.fields), test.notWant) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestValue(t *testing.T) { 62 | type want struct { 63 | value float64 64 | exists bool 65 | } 66 | tests := []struct { //nolint:govet 67 | name string 68 | Condition Condition 69 | value Field 70 | fields map[string]float64 71 | w want 72 | }{ 73 | { 74 | name: "exists", 75 | Condition: ExistsCondition(Field("a")), 76 | value: Field("a"), 77 | fields: map[string]float64{"a": 1}, 78 | w: want{1, true}, 79 | }, 80 | { 81 | name: "not exists", 82 | Condition: ExistsCondition(Field("a")), 83 | value: Field("a"), 84 | fields: map[string]float64{"b": 1}, 85 | w: want{0, false}, 86 | }, 87 | { 88 | name: "cv.Inner.Value not have fields", 89 | Condition: ExistsCondition(Field("a")), 90 | value: Field("b"), 91 | fields: map[string]float64{"b": 1}, 92 | w: want{0, false}, 93 | }, 94 | } 95 | for _, test := range tests { 96 | t.Run(test.name, func(t *testing.T) { 97 | cv := &ConditionalValue{ 98 | Condition: test.Condition, 99 | Inner: test.value, 100 | } 101 | gotVal, gotBool := cv.Value(test.fields) 102 | if gotVal != test.w.value { 103 | t.Errorf("Value() gotVal = %v, want %v", gotVal, test.w.value) 104 | } 105 | if gotBool != test.w.exists { 106 | t.Errorf("Value() gotBool = %v, want %v", gotBool, test.w.exists) 107 | } 108 | }) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /internal/scorer/algorithm/wam/wam.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package wam implements the Weighted Arithmetic Mean, which forms the 16 | // basis of Rob Pike's criticality score algorithm as documented in 17 | // Quantifying_criticality_algorithm.pdf. 18 | package wam 19 | 20 | import ( 21 | "github.com/ossf/criticality_score/v2/internal/scorer/algorithm" 22 | ) 23 | 24 | const Name = "weighted_arithmetic_mean" 25 | 26 | // "Weighted Arithmetic Mean" is also known as "Weighted Average". 27 | 28 | // WeightedArithmeticMean is an implementation of the Weighted Arithmetic Mean. 29 | // https://en.wikipedia.org/wiki/Weighted_arithmetic_mean 30 | type WeightedArithmeticMean struct { 31 | inputs []*algorithm.Input 32 | } 33 | 34 | // New returns a new instance of the Weighted Arithmetic Mean algorithm, which 35 | // is used by the Pike algorithm. 36 | func New(inputs []*algorithm.Input) (algorithm.Algorithm, error) { 37 | return &WeightedArithmeticMean{ 38 | inputs: inputs, 39 | }, nil 40 | } 41 | 42 | func (p *WeightedArithmeticMean) Score(record map[string]float64) float64 { 43 | var itemSum float64 44 | var itemCount float64 45 | for _, i := range p.inputs { 46 | if v, ok := i.Value(record); ok { 47 | itemCount += i.Weight 48 | itemSum += i.Weight * v 49 | } 50 | } 51 | return itemSum / itemCount 52 | } 53 | -------------------------------------------------------------------------------- /internal/scorer/algorithm/wam/wam_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package wam 16 | 17 | import ( 18 | "errors" 19 | "math" 20 | "testing" 21 | 22 | "github.com/ossf/criticality_score/v2/internal/scorer/algorithm" 23 | ) 24 | 25 | func TestWeighetedArithmeticMean_Score(t *testing.T) { 26 | //nolint:govet 27 | tests := []struct { 28 | name string 29 | inputs []*algorithm.Input 30 | record map[string]float64 31 | want float64 32 | err error 33 | }{ 34 | { 35 | name: "regular test", 36 | inputs: []*algorithm.Input{ 37 | { 38 | Weight: 1, Distribution: algorithm.LookupDistribution("linear"), 39 | Source: algorithm.Value(algorithm.Field("1")), 40 | }, 41 | { 42 | Weight: 2, Distribution: algorithm.LookupDistribution("linear"), 43 | Source: algorithm.Value(algorithm.Field("2")), 44 | }, 45 | { 46 | Weight: 3, Distribution: algorithm.LookupDistribution("linear"), 47 | Source: algorithm.Value(algorithm.Field("3")), 48 | }, 49 | { 50 | Weight: 4, Distribution: algorithm.LookupDistribution("linear"), 51 | Source: algorithm.Value(algorithm.Field("4")), 52 | }, 53 | { 54 | Weight: 5, Distribution: algorithm.LookupDistribution("linear"), 55 | Source: algorithm.Value(algorithm.Field("5")), 56 | }, 57 | }, 58 | want: 3.6666666666666665, 59 | record: map[string]float64{"1": 1, "2": 2, "3": 3, "4": 4, "5": 5}, 60 | }, 61 | { 62 | name: "fields not matching the record", 63 | inputs: []*algorithm.Input{ 64 | { 65 | Weight: 1, Distribution: algorithm.LookupDistribution("linear"), 66 | Source: algorithm.Value(algorithm.Field("1")), 67 | }, 68 | }, 69 | want: math.NaN(), 70 | record: map[string]float64{"2": 2}, 71 | }, 72 | { 73 | name: "some fields matching the record", 74 | inputs: []*algorithm.Input{ 75 | { 76 | Weight: 1, Distribution: algorithm.LookupDistribution("linear"), 77 | Source: algorithm.Value(algorithm.Field("1")), 78 | }, 79 | { 80 | Weight: 2, Distribution: algorithm.LookupDistribution("linear"), 81 | Source: algorithm.Value(algorithm.Field("2")), 82 | }, 83 | }, 84 | want: 1, 85 | record: map[string]float64{"1": 1}, 86 | }, 87 | } 88 | for _, test := range tests { 89 | t.Run(test.name, func(t *testing.T) { 90 | p, err := New(test.inputs) 91 | 92 | if err != nil && !errors.Is(err, test.err) { 93 | t.Errorf("New() error = %v, wantErr %v", err, test.err) 94 | } 95 | if err != nil { 96 | return 97 | } 98 | got := p.Score(test.record) 99 | 100 | if !(math.IsNaN(got) && math.IsNaN(test.want)) && got != test.want { 101 | t.Errorf("Score() = %v, want %v", got, test.want) 102 | } 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /internal/scorer/default.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package scorer 16 | 17 | import ( 18 | "bytes" 19 | _ "embed" 20 | ) 21 | 22 | const defaultScoreName = "default_score" 23 | 24 | //go:generate cp ../../config/scorer/original_pike.yml default_config.yml 25 | //go:embed default_config.yml 26 | var defaultConfigContent []byte 27 | 28 | func FromDefaultConfig() *Scorer { 29 | r := bytes.NewReader(defaultConfigContent) 30 | s, err := FromConfig(defaultScoreName, r) 31 | if err != nil { 32 | panic(err) 33 | } 34 | return s 35 | } 36 | -------------------------------------------------------------------------------- /internal/scorer/default_config.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # The original set of weights defined for the criticality score project using 16 | # Rob Pike's algorithm as implemented in the original Python project. 17 | algorithm: weighted_arithmetic_mean 18 | 19 | inputs: 20 | - field: legacy.created_since 21 | weight: 1 22 | bounds: 23 | upper: 120 24 | distribution: zipfian 25 | 26 | - field: legacy.updated_since 27 | weight: 1 28 | bounds: 29 | upper: 120 30 | smaller_is_better: yes 31 | distribution: zipfian 32 | 33 | - field: legacy.contributor_count 34 | weight: 2 35 | bounds: 36 | upper: 5000 37 | distribution: zipfian 38 | 39 | - field: legacy.org_count 40 | weight: 1 41 | bounds: 42 | upper: 10 43 | distribution: zipfian 44 | 45 | - field: legacy.commit_frequency 46 | weight: 1 47 | bounds: 48 | upper: 1000 49 | distribution: zipfian 50 | 51 | - field: legacy.recent_release_count 52 | weight: 0.5 53 | bounds: 54 | upper: 26 55 | distribution: zipfian 56 | 57 | - field: legacy.updated_issues_count 58 | weight: 0.5 59 | bounds: 60 | upper: 5000 61 | distribution: zipfian 62 | 63 | - field: legacy.closed_issues_count 64 | weight: 0.5 65 | bounds: 66 | upper: 5000 67 | distribution: zipfian 68 | 69 | - field: legacy.issue_comment_frequency 70 | weight: 1 71 | bounds: 72 | upper: 15 73 | distribution: zipfian 74 | 75 | - field: legacy.github_mention_count 76 | weight: 2 77 | bounds: 78 | upper: 500000 79 | distribution: zipfian 80 | -------------------------------------------------------------------------------- /internal/scorer/testdata/default_config.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Test Data 16 | 17 | algorithm: test 18 | 19 | inputs: 20 | - field: test 21 | distribution: linear -------------------------------------------------------------------------------- /internal/scorer/testdata/invalid_config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Criticality Score Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Test Data 16 | 17 | This shouldn't work 18 | algorithm: test 19 | inputs: 20 | - field: test 21 | distribution: 22 | linear -------------------------------------------------------------------------------- /internal/signalio/csv.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package signalio 16 | 17 | import ( 18 | "encoding/csv" 19 | "io" 20 | "sync" 21 | 22 | "github.com/ossf/criticality_score/v2/internal/collector/signal" 23 | ) 24 | 25 | type csvWriter struct { 26 | w *csv.Writer 27 | header []string 28 | headerWritten bool 29 | 30 | // Prevents concurrent writes to w, and headerWritten. 31 | mu sync.Mutex 32 | } 33 | 34 | func CSVWriter(w io.Writer, emptySets []signal.Set, extra ...string) Writer { 35 | return &csvWriter{ 36 | header: fieldsFromSignalSets(emptySets, extra), 37 | w: csv.NewWriter(w), 38 | } 39 | } 40 | 41 | // WriteSignals implements the Writer interface. 42 | func (w *csvWriter) WriteSignals(signals []signal.Set, extra ...Field) error { 43 | values, err := marshalToMap(signals, extra...) 44 | if err != nil { 45 | return err 46 | } 47 | return w.writeRecord(values) 48 | } 49 | 50 | func (w *csvWriter) maybeWriteHeader() error { 51 | // Check headerWritten without the lock to avoid holding the lock if the 52 | // header has already been written. 53 | if w.headerWritten { 54 | return nil 55 | } 56 | // Grab the lock and re-check headerWritten just in case another goroutine 57 | // entered the same critical section. Also prevent concurrent writes to w. 58 | w.mu.Lock() 59 | defer w.mu.Unlock() 60 | if w.headerWritten { 61 | return nil 62 | } 63 | w.headerWritten = true 64 | return w.w.Write(w.header) 65 | } 66 | 67 | func (w *csvWriter) writeRecord(values map[string]string) error { 68 | if err := w.maybeWriteHeader(); err != nil { 69 | return err 70 | } 71 | var rec []string 72 | for _, k := range w.header { 73 | rec = append(rec, values[k]) 74 | } 75 | // Grab the lock when we're ready to write the record to prevent 76 | // concurrent writes to w. 77 | w.mu.Lock() 78 | defer w.mu.Unlock() 79 | if err := w.w.Write(rec); err != nil { 80 | return err 81 | } 82 | w.w.Flush() 83 | return w.w.Error() 84 | } 85 | -------------------------------------------------------------------------------- /internal/signalio/helpers.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // https://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package signalio 14 | 15 | import ( 16 | "fmt" 17 | "time" 18 | 19 | "github.com/ossf/criticality_score/v2/internal/collector/signal" 20 | ) 21 | 22 | func fieldsFromSignalSets(sets []signal.Set, extra []string) []string { 23 | var fields []string 24 | for _, s := range sets { 25 | if err := signal.ValidateSet(s); err != nil { 26 | panic(err) 27 | } 28 | fields = append(fields, signal.SetFields(s, true)...) 29 | } 30 | // Append all the extra fields 31 | fields = append(fields, extra...) 32 | return fields 33 | } 34 | 35 | func marshalToMap(signals []signal.Set, extra ...Field) (map[string]string, error) { 36 | values := make(map[string]string) 37 | for _, s := range signals { 38 | // Get all of the signal data from the set and serialize it. 39 | for k, v := range signal.SetAsMap(s, true) { 40 | if s, err := marshalValue(v); err != nil { 41 | return nil, fmt.Errorf("failed to write field %s: %w", k, err) 42 | } else { 43 | values[k] = s 44 | } 45 | } 46 | } 47 | for _, f := range extra { 48 | if s, err := marshalValue(f.Value); err != nil { 49 | return nil, fmt.Errorf("failed to write field %s: %w", f.Key, err) 50 | } else { 51 | values[f.Key] = s 52 | } 53 | } 54 | return values, nil 55 | } 56 | 57 | func marshalValue(value any) (string, error) { 58 | switch v := value.(type) { 59 | case bool, int, int16, int32, int64, uint, uint16, uint32, uint64, byte, float32, float64, string: 60 | return fmt.Sprintf("%v", value), nil 61 | case time.Time: 62 | return v.Format(time.RFC3339), nil 63 | case nil: 64 | return "", nil 65 | default: 66 | return "", fmt.Errorf("%w: %T", ErrorMarshalFailure, value) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/signalio/json.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package signalio 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "io" 21 | "sync" 22 | 23 | "github.com/ossf/criticality_score/v2/internal/collector/signal" 24 | ) 25 | 26 | type jsonWriter struct { 27 | encoder *json.Encoder 28 | 29 | // Prevents concurrent writes to w. 30 | mu sync.Mutex 31 | } 32 | 33 | func JSONWriter(w io.Writer) Writer { 34 | e := json.NewEncoder(w) 35 | e.SetIndent("", "") 36 | return &jsonWriter{ 37 | encoder: e, 38 | } 39 | } 40 | 41 | // WriteSignals implements the Writer interface. 42 | func (w *jsonWriter) WriteSignals(signals []signal.Set, extra ...Field) error { 43 | data := make(map[string]any) 44 | for _, s := range signals { 45 | m := signal.SetAsMapWithNamespace(s) 46 | 47 | // Merge m with data 48 | for ns, innerM := range m { 49 | d, ok := data[ns] 50 | if !ok { 51 | d = make(map[string]any) 52 | data[ns] = d 53 | } 54 | nsData, ok := d.(map[string]any) 55 | if !ok { 56 | return fmt.Errorf("failed to get map for namespace: %s", ns) 57 | } 58 | for k, v := range innerM { 59 | nsData[k] = v 60 | } 61 | } 62 | } 63 | for _, f := range extra { 64 | data[f.Key] = f.Value 65 | } 66 | w.mu.Lock() 67 | defer w.mu.Unlock() 68 | return w.encoder.Encode(data) 69 | } 70 | -------------------------------------------------------------------------------- /internal/signalio/text.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package signalio 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "sync" 21 | 22 | "github.com/ossf/criticality_score/v2/internal/collector/signal" 23 | ) 24 | 25 | type textWriter struct { 26 | w io.Writer 27 | fields []string 28 | firstRecordWritten bool 29 | 30 | // Prevents concurrent writes to w. 31 | mu sync.Mutex 32 | } 33 | 34 | func TextWriter(w io.Writer, emptySets []signal.Set, extra ...string) Writer { 35 | return &textWriter{ 36 | w: w, 37 | fields: fieldsFromSignalSets(emptySets, extra), 38 | } 39 | } 40 | 41 | // WriteSignals implements the Writer interface. 42 | func (w *textWriter) WriteSignals(signals []signal.Set, extra ...Field) error { 43 | values, err := marshalToMap(signals, extra...) 44 | if err != nil { 45 | return err 46 | } 47 | return w.writeRecord(values) 48 | } 49 | 50 | func (w *textWriter) writeRecord(values map[string]string) error { 51 | w.mu.Lock() 52 | defer w.mu.Unlock() 53 | 54 | // Output a newline between records if this isn't the first record. 55 | if w.firstRecordWritten { 56 | _, err := fmt.Fprintln(w.w, "") 57 | if err != nil { 58 | return err 59 | } 60 | } else { 61 | w.firstRecordWritten = true 62 | } 63 | 64 | for _, field := range w.fields { 65 | val := values[field] 66 | _, err := fmt.Fprintf(w.w, "%s: %s\n", field, val) 67 | if err != nil { 68 | return err 69 | } 70 | } 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/signalio/type.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package signalio 16 | 17 | import ( 18 | "bytes" 19 | "errors" 20 | "io" 21 | 22 | "github.com/ossf/criticality_score/v2/internal/collector/signal" 23 | ) 24 | 25 | type WriterType int 26 | 27 | const ( 28 | WriterTypeCSV = WriterType(iota) 29 | WriterTypeJSON 30 | WriterTypeText 31 | ) 32 | 33 | var ErrorUnknownWriterType = errors.New("unknown writer type") 34 | 35 | // String implements the fmt.Stringer interface. 36 | func (t WriterType) String() string { 37 | text, err := t.MarshalText() 38 | if err != nil { 39 | return "" 40 | } 41 | return string(text) 42 | } 43 | 44 | // MarshalText implements the encoding.TextMarshaler interface. 45 | func (t WriterType) MarshalText() ([]byte, error) { 46 | switch t { 47 | case WriterTypeCSV: 48 | return []byte("csv"), nil 49 | case WriterTypeJSON: 50 | return []byte("json"), nil 51 | case WriterTypeText: 52 | return []byte("text"), nil 53 | default: 54 | return []byte{}, ErrorUnknownWriterType 55 | } 56 | } 57 | 58 | // UnmarshalText implements the encoding.TextUnmarshaler interface. 59 | func (t *WriterType) UnmarshalText(text []byte) error { 60 | switch { 61 | case bytes.Equal(text, []byte("csv")): 62 | *t = WriterTypeCSV 63 | case bytes.Equal(text, []byte("json")): 64 | *t = WriterTypeJSON 65 | case bytes.Equal(text, []byte("text")): 66 | *t = WriterTypeText 67 | default: 68 | return ErrorUnknownWriterType 69 | } 70 | return nil 71 | } 72 | 73 | // New will return a new instance of the corresponding implementation of 74 | // Writer for the given WriterType. 75 | func (t *WriterType) New(w io.Writer, emptySets []signal.Set, extra ...string) Writer { 76 | switch *t { 77 | case WriterTypeCSV: 78 | return CSVWriter(w, emptySets, extra...) 79 | case WriterTypeJSON: 80 | return JSONWriter(w) 81 | case WriterTypeText: 82 | return TextWriter(w, emptySets, extra...) 83 | default: 84 | return nil 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /internal/signalio/writer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package signalio 16 | 17 | import ( 18 | "errors" 19 | 20 | "github.com/ossf/criticality_score/v2/internal/collector/signal" 21 | ) 22 | 23 | var ErrorMarshalFailure = errors.New("failed to marshal value") 24 | 25 | //nolint:govet 26 | type Field struct { 27 | Key string 28 | Value any 29 | } 30 | 31 | type Writer interface { 32 | // WriteSignals outputs the all the signals collector to storage. 33 | WriteSignals([]signal.Set, ...Field) error 34 | } 35 | -------------------------------------------------------------------------------- /internal/workerpool/workerpool.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package workerpool 16 | 17 | import ( 18 | "sync" 19 | ) 20 | 21 | // WorkerFunc implements a function that can be used in a WorkerPool. 22 | // 23 | // worker is a unique integer identifying the worker, starting at 0. 24 | type WorkerFunc func(worker int) 25 | 26 | // WorkerPool starts a pool of n workers each running the WorkerFunc w. 27 | // 28 | // Returns a function waitFunc, that when called waits for all workers to 29 | // finish. 30 | func WorkerPool(n int, w WorkerFunc) (waitFunc func()) { 31 | wg := &sync.WaitGroup{} 32 | wg.Add(n) 33 | for i := 0; i < n; i++ { 34 | go func(worker int) { 35 | defer wg.Done() 36 | w(worker) 37 | }(i) 38 | } 39 | return wg.Wait 40 | } 41 | -------------------------------------------------------------------------------- /internal/workerpool/workerpool_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package workerpool_test 16 | 17 | import ( 18 | "sync/atomic" 19 | "testing" 20 | 21 | "github.com/ossf/criticality_score/v2/internal/workerpool" 22 | ) 23 | 24 | func TestOneWorker(t *testing.T) { 25 | var counter int32 26 | wait := workerpool.WorkerPool(1, func(worker int) { 27 | atomic.AddInt32(&counter, 1) 28 | }) 29 | wait() 30 | if counter != 1 { 31 | t.Fatalf("counter = %d; want 1", counter) 32 | } 33 | } 34 | 35 | func TestManyWorkers(t *testing.T) { 36 | var counter int32 37 | wait := workerpool.WorkerPool(10, func(worker int) { 38 | atomic.AddInt32(&counter, 1) 39 | }) 40 | wait() 41 | if counter != 10 { 42 | t.Fatalf("counter = %d; want 10", counter) 43 | } 44 | } 45 | 46 | func TestUniqueWorkerId(t *testing.T) { 47 | var counters [10]int32 48 | wait := workerpool.WorkerPool(10, func(worker int) { 49 | atomic.AddInt32(&(counters[worker]), 1) 50 | }) 51 | wait() 52 | for worker, counter := range counters { 53 | if counter != 1 { 54 | t.Fatalf("counters[%d] = %d; want 1", worker, counter) 55 | } 56 | } 57 | } 58 | 59 | func TestExampleWorkload(t *testing.T) { 60 | nums := make(chan int) 61 | doubles := make(chan int) 62 | done := make(chan bool) 63 | var results []int 64 | 65 | // Consume the doubles channel 66 | go func() { 67 | for i := range doubles { 68 | results = append(results, i) 69 | } 70 | done <- true 71 | }() 72 | 73 | // Start a pool of workers 74 | wait := workerpool.WorkerPool(5, func(worker int) { 75 | for n := range nums { 76 | doubles <- n * 2 77 | } 78 | }) 79 | 80 | // Send 0-9 into the nums channel 81 | for i := 0; i < 10; i++ { 82 | nums <- i 83 | } 84 | 85 | // Close nums which causes the workers to quit 86 | close(nums) 87 | 88 | // Wait for all the workers to be finished 89 | wait() 90 | 91 | // Close the doubles channels to terminate the consumer. 92 | close(doubles) 93 | 94 | // Wait for the consumer to be finished. 95 | <-done 96 | 97 | // Make sure all were generated 98 | if l := len(results); l != 10 { 99 | t.Fatalf("len(results) = %d; want 10", l) 100 | } 101 | // Make sure the results are actually doubles 102 | for _, r := range results { 103 | if r%2 != 0 || r < 0 || r/2 >= 10 { 104 | t.Fatalf("result = %d, want result to be divisible by 2, and less than 20", r) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /output/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /scripts/validate-scorecard-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2022 Criticality Score Authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # Validate that the commit assigned to the Go dependency of Scorecard matches 17 | # the production Docker images used when running Criticality Score. 18 | # 19 | # This check ensures that there is no version skew against Scorecard. Since 20 | # our code depends on the behavior of various Scorecard cron libraries it is 21 | # important that the production images built by the Scorecard project remain 22 | # consistent. 23 | # 24 | # In particular the way config.yaml files are parsed, messages are passed 25 | # between the controller and worker, or the shard file structure, may change in 26 | # ways that break Criticality Score if the versions are not kept in sync. 27 | 28 | # Get the path to a Go binary. This should be available in the GitHub Action images. 29 | GO_BIN=$(which go) 30 | if [ "$GO_BIN" = "" ]; then 31 | echo "Failed to find GO binary." 32 | exit 1 33 | fi 34 | 35 | # The location of the kustomization file with the Scorecard image tags. 36 | KUSTOMIZATION_FILE=infra/envs/base/kustomization.yaml 37 | 38 | KUSTOMIZATION_KEY="newTag:" 39 | SCORECARD_REPO="ossf/scorecard" 40 | SCORECARD_IMPORT="github.com/$SCORECARD_REPO/v4" 41 | 42 | # Get image tags from the kustomization file 43 | IMAGE_TAGS=$(grep "$KUSTOMIZATION_KEY" "$KUSTOMIZATION_FILE" | sort | uniq | sed "s/^[[:space:]]*$KUSTOMIZATION_KEY[[:space:]]*//") 44 | 45 | # Get the scorecard version 46 | SCORECARD_VERSION=$($GO_BIN list -m -f '{{ .Version }}' $SCORECARD_IMPORT) 47 | if [ "$SCORECARD_VERSION" = "" ]; then 48 | echo "Scorecard dependency missing." 49 | exit 1 50 | fi 51 | 52 | # Test 1: Ensure the same tag is used for each kubernetes image. 53 | TAG_COUNT=$(echo "$IMAGE_TAGS" | wc -l) 54 | if [ $TAG_COUNT -ne 1 ]; then 55 | echo "$KUSTOMIZATION_FILE: Scorecard image tags found: $TAG_COUNT. Want: 1" 56 | exit 1 57 | fi 58 | 59 | # Test 2: Extract the origin hash from the module. 60 | COMMIT_SHA=$($GO_BIN list -m -f '{{ .Origin.Hash }}' "$SCORECARD_IMPORT@$SCORECARD_VERSION") 61 | if [ "$?" != "0" ]; then 62 | echo "Go go.mod $COMMIT_SHA does not match $KUSTOMIZATION_FILE $IMAGE_TAGS" 63 | exit 1 64 | elif [ "$COMMIT_SHA" = "$IMAGE_TAGS" ]; then 65 | exit 0 66 | elif [ "$COMMIT_SHA" != "" ]; then 67 | echo "Go go.mod $COMMIT_SHA does not match $KUSTOMIZATION_FILE $IMAGE_TAGS" 68 | exit 1 69 | fi 70 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Criticality Score Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | // This set of imports is used to ensure `go mod tidy` does not remove them. 18 | import ( 19 | _ "github.com/golangci/golangci-lint/v2/cmd/golangci-lint" 20 | _ "mvdan.cc/gofumpt" 21 | ) 22 | --------------------------------------------------------------------------------