diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 06caa5d6..1db4a5ba 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,31 +11,61 @@ env: jobs: build: runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') steps: - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Go Build Cache for Docker + uses: actions/cache@v3 + with: + path: go-build-cache + key: ${{ runner.os }}-go-build-cache-${{ hashFiles('go.sum') }} + + - name: inject go-build-cache into docker + # v1 was composed of two actions: "inject" and "extract". + # v2 is unified to a single action. + uses: reproducible-containers/buildkit-cache-dance@v2.1.2 + with: + cache-source: go-build-cache + - name: Set up Docker buildx - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 + + - name: Get the tag or commit id + id: version + run: | + if [[ $GITHUB_REF == refs/tags/* ]]; then + # If a tag is present, strip the 'refs/tags/' prefix + TAG_OR_COMMIT=$(echo $GITHUB_REF | sed 's/refs\/tags\///') + echo "This is a tag: $TAG_OR_COMMIT" + else + # If no tag is present, use the commit SHA + TAG_OR_COMMIT=$(echo $GITHUB_SHA) + echo "This is a commit SHA: $TAG_OR_COMMIT" + fi + # Set the variable for use in other steps + echo "TAG_OR_COMMIT=$TAG_OR_COMMIT" >> $GITHUB_OUTPUT + shell: bash + + - name: Build and push + uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0 with: context: . push: true tags: ${{ env.GITHUB_IMAGE_NAME }} + build-args: | + BUILD_VERSION=${{ steps.version.outputs.TAG_OR_COMMIT }} cache-from: type=gha cache-to: type=gha,mode=max - platforms: linux/amd64,linux/arm64 - - uses: actions/checkout@v2 + # platforms: linux/amd64,linux/arm64 - name: Slack Notification - uses: rtCamp/action-slack-notify@v2 + uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 # v2.2.0 env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} SLACK_MESSAGE: "Pushed to ${{ env.GITHUB_IMAGE_NAME }}" diff --git a/.github/workflows/sqlc.yml b/.github/workflows/sqlc.yml deleted file mode 100644 index 297d8cfe..00000000 --- a/.github/workflows/sqlc.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: sqlc - -on: [push] - -jobs: - testing: - runs-on: ubuntu-latest - - steps: - - name: Checkout upstream repo - uses: actions/checkout@v2 - with: - ref: ${{ github.head_ref }} - - uses: sqlc-dev/setup-sqlc@v4 - with: - sqlc-version: "1.21.0" - - run: sqlc diff diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 827617c7..06f4ab7b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,12 +24,6 @@ jobs: - uses: actions/setup-go@v4 with: go-version-file: "go.mod" - - run: | - curl -Lsf -O https://github.com/k0kubun/sqldef/releases/download/v0.16.5/psqldef_linux_amd64.tar.gz - tar xzf psqldef_linux_amd64.tar.gz - ./psqldef -U pguser -f ./database/schema.sql testdb - env: - PGPASSWORD: pgpass - run: go test --tags github ./... env: TEST_DB_DSN: "user=pguser password=pgpass dbname=testdb sslmode=disable" diff --git a/.gitignore b/.gitignore index 16de438e..8345a4cd 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ assets/.next /*.json tmp +/pkg/usecase/templates/test_* trivy.db octovy diff --git a/Dockerfile b/Dockerfile index 277c2d98..c73521e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,24 @@ -FROM golang:1.21-bullseye AS build-go -COPY . /app +FROM golang:1.22 AS build-go +ENV CGO_ENABLED=0 +ARG BUILD_VERSION + WORKDIR /app -# ENV CGO_ENABLED=0 -RUN go get -v -RUN go build . +RUN go env -w GOMODCACHE=/root/.cache/go-build + +COPY go.mod go.sum ./ +RUN --mount=type=cache,target=/root/.cache/go-build go mod download + +COPY . /app +RUN --mount=type=cache,target=/root/.cache/go-build go build -o octovy -ldflags "-X github.com/m-mizutani/octovy/pkg/domain/types.AppVersion=${BUILD_VERSION}" . FROM gcr.io/distroless/base:nonroot +USER nonroot COPY --from=build-go /app/octovy /octovy -COPY --from=build-go /app/database /database -COPY --from=aquasec/trivy:0.45.1 /usr/local/bin/trivy /trivy +COPY --from=aquasec/trivy:0.50.4 /usr/local/bin/trivy /trivy WORKDIR / ENV OCTOVY_ADDR="0.0.0.0:8000" ENV OCTOVY_TRIVY_PATH=/trivy EXPOSE 8000 -ENTRYPOINT [ "/octovy" ] + +ENTRYPOINT ["/octovy"] + diff --git a/README.md b/README.md index 1f5de8dd..4fd02a80 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,15 @@ # Octovy -Octovy is a GitHub application designed to identify and alert you to any dependencies in your repository that could be potentially vulnerable. It uses [trivy](https://github.com/aquasecurity/trivy) for detection and then stores the results in a database for your reference. +Octovy is a GitHub App that scans your repository's code for potentially vulnerable dependencies. It utilizes [trivy](https://github.com/aquasecurity/trivy) to detect software vulnerabilities. When triggered by events like `push` and `pull_request` from GitHub, Octovy scans the repository for dependency vulnerabilities and performs the following actions: -![architecture](https://github.com/m-mizutani/octovy/assets/605953/a58c93e1-cfbf-4ff7-9427-1fc385cf7b9c) +- Adds a comment to the pull request, summarizing the vulnerabilities found +- Inserts the scan results into BigQuery + +![architecture](https://github.com/m-mizutani/octovy/assets/605953/4366161f-a4ff-4abb-9766-0fb4df818cb1) + +Octovy adds a comment to the pull request when it detects new vulnerabilities between the head of the PR and the merge destination. + +comment example ## Setup @@ -16,6 +23,7 @@ Start by creating a GitHub App [here](https://github.com/settings/apps). You can - **Permissions & events** - Repository Permissions + - **Checks**: Set to Read & Write - **Contents**: Set to Read-only - **Metadata**: Set to Read-only - **Pull Requests**: Set to Read & Write @@ -23,22 +31,15 @@ Start by creating a GitHub App [here](https://github.com/settings/apps). You can - **Pull request** - **Push** -Once complete, note down the following information from the **General** section for later: +Once you have completed the setup, make sure to take note of the following information from the **General** section for future reference: - **App ID** (e.g. `123456`) - **Private Key**: Click `Generate a private key` and download the key file (e.g. `your-app-name.2023-08-14.private-key.pem`) -### 2. Setting Up the Database - -Octovy requires a PostgreSQL database. You can use any PostgreSQL instance you like, but we recommend cloud-based database services such as [Google Cloud SQL](https://cloud.google.com/sql) or [Amazon RDS](https://aws.amazon.com/rds/). - -For database migration, [sqldef](https://github.com/k0kubun/sqldef) is recommended. After installing sqldef, you can migrate your database schema using the command below. Be sure to replace the placeholders with your actual database information. +### 2. Setting Up Cloud Resources -```bash -# NOTICE: Be careful not to save the password to shell history -$ export PGPASSWORD=[db_password] -$ psqldef -U [db_user] -p [db_port] -h [db_host] -f database/schema.sql [db_name] -``` +- **Cloud Storage**: Create a Cloud Storage bucket dedicated to storing the scan results exclusively for Octovy's use. +- **BigQuery** (Optional): Create a BigQuery dataset and table for storing the scan results. Octovy will automatically update the schema. The default table name should be `scans`. ### 3. Deploying Octovy @@ -46,21 +47,22 @@ The recommended method of deploying Octovy is via a container image, available a To run Octovy, set the following environment variables: -- GitHub App - - `OCTOVY_GITHUB_APP_ID`: App ID of your GitHub App - - `OCTOVY_GITHUB_APP_PRIVATE_KEY`: Private key of your GitHub App - - `OCTOVY_GITHUB_SECRET`: Webhook secret of your GitHub App -- Network - - `OCTOVY_ADDR`: Listening address (e.g. `0.0.0.0:8080`) -- Database - - `OCTOVY_DB_HOST`: Hostname of your PostgreSQL database - - `OCTOVY_DB_PORT`: Port number of your PostgreSQL database - - `OCTOVY_DB_USER`: Username of your PostgreSQL database - - `OCTOVY_DB_PASSWORD`: Password of your PostgreSQL database - - `OCTOVY_DB_NAME`: Database name of your PostgreSQL database -- Logging - - `OCTOVY_LOG_LEVEL`: Log level (e.g. `debug`, `info`, `warn`, `error`) - - `OCTOVY_LOG_FORMAT`: Log format, recommend to use `json` +#### Required Environment Variables +- `OCTOVY_ADDR`: The address to bind the server to (e.g. `:8080`) +- `OCTOVY_GITHUB_APP_ID`: The GitHub App ID +- `OCTOVY_GITHUB_APP_PRIVATE_KEY`: The path to the private key file +- `OCTOVY_GITHUB_APP_SECRET`: The secret string used to verify the webhook request from GitHub +- `OCTOVY_CLOUD_STORAGE_BUCKET`: The name of the Cloud Storage bucket + +#### Optional Environment Variables +- `OCTOVY_TRIVY_PATH`: The path to the trivy binary. If you uses the our container image, you don't need to set this variable. +- `OCTOVY_CLOUD_STORAGE_PREFIX`: The prefix for the Cloud Storage object +- `OCTOVY_BIGQUERY_PROJECT_ID`: The name of the BigQuery dataset +- `OCTOVY_BIGQUERY_DATASET_ID`: The name of the BigQuery table +- `OCTOVY_BIGQUERY_TABLE_ID`: The name of the BigQuery table +- `OCTOVY_BIGQUERY_IMPERSONATE_SERVICE_ACCOUNT`: The service account to impersonate when accessing BigQuery +- `OCTOVY_SENTRY_DSN`: The DSN for Sentry +- `OCTOVY_SENTRY_ENV`: The environment for Sentry ## License diff --git a/database/embed.go b/database/embed.go deleted file mode 100644 index daf4fce2..00000000 --- a/database/embed.go +++ /dev/null @@ -1,12 +0,0 @@ -package database - -import ( - _ "embed" -) - -//go:embed schema.sql -var schema string - -func Schema() string { - return schema -} diff --git a/database/query.sql b/database/query.sql deleted file mode 100644 index 6f4a6f6c..00000000 --- a/database/query.sql +++ /dev/null @@ -1,125 +0,0 @@ --- name: SaveScan :exec -INSERT INTO scans ( - id, - artifact_name, - artifact_type -) VALUES ( - $1, $2, $3 -); - --- name: SaveMetaGithubRepository :exec -INSERT INTO meta_github_repository ( - id, - scan_id, - repository_id, - branch, - is_default_branch, - commit_id, - base_commit_id, - pull_request_id -) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8 -); - --- name: SaveGithubRepository :one -WITH ins AS ( - INSERT INTO github_repository ( - id, - repo_id, - owner, - repo_name - ) VALUES ( - $1, $2, $3, $4 - ) ON CONFLICT (repo_id) DO NOTHING - RETURNING id -) -SELECT id FROM ins -UNION ALL -SELECT id FROM github_repository WHERE repo_id = $2 AND NOT EXISTS (SELECT 1 FROM ins); - --- name: SaveResult :exec -INSERT INTO results ( - id, - scan_id, - target, - target_type, - class -) VALUES ( - $1, $2, $3, $4, $5 -); - --- name: SavePackage :exec -INSERT INTO packages ( - id, - target_type, - name, - version -) VALUES ( - $1, $2, $3, $4 -); - --- name: GetPackages :many -SELECT * FROM packages WHERE id = ANY($1::text[]); - --- name: SaveDetectedPackage :exec -INSERT INTO detected_packages ( - id, - result_id, - pkg_id -) VALUES ( - $1, $2, $3 -); - --- name: SaveVulnerability :exec -INSERT INTO vulnerabilities ( - id, - title, - severity, - published_at, - last_modified_at, - data -) VALUES ( - $1, $2, $3, $4, $5, $6 -) ON CONFLICT (id) -DO UPDATE SET - title = $2, - severity = $3, - published_at = $4, - last_modified_at = $5, - data = $6 -WHERE vulnerabilities.last_modified_at < $5; - --- name: GetVulnerability :one -SELECT * FROM vulnerabilities WHERE id = $1; - --- name: GetVulnerabilities :many -SELECT * FROM vulnerabilities WHERE id = ANY($1::text[]); - --- name: SaveDetectedVulnerability :exec -INSERT INTO detected_vulnerabilities ( - id, - result_id, - vuln_id, - pkg_id, - installed_version, - fixed_version, - data -) VALUES ( - $1, $2, $3, $4, $5, $6, $7 -); - --- name: GetLatestResultsByCommit :many -SELECT results.* FROM results -INNER JOIN ( - SELECT scans.id AS id FROM meta_github_repository - INNER JOIN scans ON scans.id = meta_github_repository.scan_id - INNER JOIN github_repository ON github_repository.id = meta_github_repository.repository_id - WHERE meta_github_repository.commit_id = $1 - AND github_repository.repo_id = $2 - ORDER BY scans.created_at DESC - LIMIT 1 -) AS latest_scan ON latest_scan.id = results.scan_id; - --- name: GetVulnerabilitiesByResultID :many -SELECT detected_vulnerabilities.* FROM detected_vulnerabilities -WHERE detected_vulnerabilities.result_id = $1; diff --git a/database/schema.sql b/database/schema.sql deleted file mode 100644 index b82472ce..00000000 --- a/database/schema.sql +++ /dev/null @@ -1,87 +0,0 @@ -CREATE TYPE target_class AS ENUM ('os-pkgs', 'lang-pkgs'); - -create table scans ( - id uuid primary key not null, - created_at timestamp with time zone not null default now(), - artifact_name text not null, - artifact_type text not null, - - page_seq serial -); - -create table github_repository ( - id uuid primary key not null, - - -- this id is the same as the id in GitHub - repo_id bigint not null unique, - owner text not null, - repo_name text not null, - - page_seq serial -); - -create index github_repository_repo_id on github_repository (repo_id); - -create table meta_github_repository ( - id uuid primary key not null, - scan_id uuid not null references scans(id), - - repository_id uuid not null references github_repository(id), - commit_id text not null, - branch text, - is_default_branch boolean, - base_commit_id text, - pull_request_id int, - - page_seq serial -); - -CREATE INDEX meta_github_repository_commit ON meta_github_repository (commit_id); - -create table results ( - id uuid primary key not null, - scan_id uuid not null references scans(id), - - target text not null, - target_type text not null, - class target_class not null -); - -create table packages ( - -- id is hash of target_type, name, version - id text primary key not null, - - target_type text not null, - name text not null, - version text not null -); - -create table detected_packages ( - id uuid primary key not null, - result_id uuid not null references results(id), - pkg_id text not null references packages(id) -); - -create table vulnerabilities ( - id text primary key not null, - - title text not null, - severity text not null, - published_at timestamp with time zone, - last_modified_at timestamp with time zone, - - data JSONB, - page_seq serial -); - -create table detected_vulnerabilities ( - id uuid primary key not null, - result_id uuid not null references results(id), - vuln_id text not null references vulnerabilities(id), - - pkg_id text not null references packages(id), - fixed_version text, - installed_version text, - - data JSONB -); diff --git a/go.mod b/go.mod index 343363f3..254f4518 100644 --- a/go.mod +++ b/go.mod @@ -1,53 +1,104 @@ module github.com/m-mizutani/octovy -go 1.21 +go 1.22 + +toolchain go1.22.0 require ( - github.com/aquasecurity/trivy v0.45.1 - github.com/aquasecurity/trivy-db v0.0.0-20231005141211-4fc651f7ac8d - github.com/bradleyfalzon/ghinstallation/v2 v2.7.0 - github.com/fatih/color v1.15.0 - github.com/go-chi/chi/v5 v5.0.10 + cloud.google.com/go/bigquery v1.61.0 + cloud.google.com/go/storage v1.41.0 + github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 + github.com/fatih/color v1.17.0 + github.com/getsentry/sentry-go v0.28.0 + github.com/go-chi/chi/v5 v5.0.12 github.com/google/go-github/v53 v53.2.0 - github.com/google/uuid v1.3.1 - github.com/k0kubun/sqldef v0.16.9 + github.com/google/uuid v1.6.0 github.com/lib/pq v1.10.9 + github.com/m-mizutani/bqs v0.0.6 github.com/m-mizutani/clog v0.0.4 - github.com/m-mizutani/goerr v0.1.11 + github.com/m-mizutani/goerr v0.1.13 github.com/m-mizutani/gots v0.0.0-20230529013424-0639119b2cdd - github.com/m-mizutani/gt v0.0.8-0.20231008050155-f6c8702e116a - github.com/m-mizutani/masq v0.1.5 - github.com/sqlc-dev/pqtype v0.3.0 - github.com/urfave/cli/v2 v2.25.7 + github.com/m-mizutani/gt v0.0.10 + github.com/m-mizutani/masq v0.1.8 + github.com/m-mizutani/opac v0.2.0 + github.com/urfave/cli/v2 v2.27.2 + google.golang.org/api v0.183.0 + google.golang.org/protobuf v1.34.1 ) require ( - github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect - github.com/anchore/go-struct-converter v0.0.0-20230627203149-c72ef8859ca9 // indirect - github.com/cloudflare/circl v1.3.3 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + cloud.google.com/go v0.114.0 // indirect + cloud.google.com/go/auth v0.5.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/iam v1.1.8 // indirect + github.com/OneOfOne/xxhash v1.2.8 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/agnivade/levenshtein v1.1.1 // indirect + github.com/apache/arrow/go/v15 v15.0.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudflare/circl v1.3.8 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/goccy/go-json v0.10.3 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.5.9 // indirect - github.com/google/go-containerregistry v0.16.1 // indirect - github.com/google/go-github/v55 v55.0.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/flatbuffers v24.3.25+incompatible // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-github/v62 v62.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.4 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/k0kubun/pp/v3 v3.2.0 // indirect - github.com/kr/pretty v0.3.1 // indirect + github.com/klauspost/compress v1.17.8 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/pganalyze/pg_query_go/v4 v4.2.3 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/open-policy-agent/opa v0.65.0 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.54.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/samber/lo v1.38.1 // indirect - github.com/spdx/tools-golang v0.5.3 // indirect - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/oauth2 v0.13.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.31.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tchap/go-patricia/v2 v2.3.1 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + github.com/yashtewari/glob-intersection v0.2.0 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect + go.opentelemetry.io/otel v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/sdk v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.27.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.22.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + google.golang.org/genproto v0.0.0-20240604185151-ef581f913117 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect + google.golang.org/grpc v1.64.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 7322573b..5e40a1b1 100644 --- a/go.sum +++ b/go.sum @@ -1,189 +1,377 @@ -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= -github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE= -github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= -github.com/anchore/go-struct-converter v0.0.0-20230627203149-c72ef8859ca9 h1:6COpXWpHbhWM1wgcQN95TdsmrLTba8KQfPgImBXzkjA= -github.com/anchore/go-struct-converter v0.0.0-20230627203149-c72ef8859ca9/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= -github.com/aquasecurity/trivy v0.45.1 h1:JjkrawgNpVUV6mxtFX635I3MhzDqmGkze46SnygkYN0= -github.com/aquasecurity/trivy v0.45.1/go.mod h1:3cawI6q9o32pPGXhuGEIWWwUCSMAzRk/FhsEdA4eW1k= -github.com/aquasecurity/trivy-db v0.0.0-20231005141211-4fc651f7ac8d h1:fjI9mkoTUAkbGqpzt9nJsO24RAdfG+ZSiLFj0G2jO8c= -github.com/aquasecurity/trivy-db v0.0.0-20231005141211-4fc651f7ac8d/go.mod h1:cj9/QmD9N3OZnKQMp+/DvdV+ym3HyIkd4e+F0ZM3ZGs= -github.com/bradleyfalzon/ghinstallation/v2 v2.7.0 h1:ranXaC3Zz/F6G/f0Joj3LrFp2OzOKfJZev5Q7OaMc88= -github.com/bradleyfalzon/ghinstallation/v2 v2.7.0/go.mod h1:ymxfmloxXBFXvvF1KpeUhOQM6Dfz9NYtfvTiJyk82UE= -github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.114.0 h1:OIPFAdfrFDFO2ve2U7r/H5SwSbBzEdrBdE7xkgwc+kY= +cloud.google.com/go v0.114.0/go.mod h1:ZV9La5YYxctro1HTPug5lXH/GefROyW8PPD4T8n9J8E= +cloud.google.com/go/auth v0.5.1 h1:0QNO7VThG54LUzKiQxv8C6x1YX7lUrzlAa1nVLF8CIw= +cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s= +cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/bigquery v1.61.0 h1:w2Goy9n6gh91LVi6B2Sc+HpBl8WbWhIyzdvVvrAuEIw= +cloud.google.com/go/bigquery v1.61.0/go.mod h1:PjZUje0IocbuTOdq4DBOJLNYB0WF3pAKBHzAYyxCwFo= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/datacatalog v1.20.1 h1:czcba5mxwRM5V//jSadyig0y+8aOHmN7gUl9GbHu59E= +cloud.google.com/go/datacatalog v1.20.1/go.mod h1:Jzc2CoHudhuZhpv78UBAjMEg3w7I9jHA11SbRshWUjk= +cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= +cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= +cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= +cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= +cloud.google.com/go/storage v1.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0= +cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= +github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= +github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= +github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 h1:R9d0v+iobRHSaE4wKUnXFiZp53AL4ED5MzgEMwGTZag= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0/go.mod h1:0LWKQwOHewXO/1acI6TtyE0Xc4ObDb2rFN7eHBAG71M= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= -github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= +github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= +github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= -github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= -github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg= +github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/getsentry/sentry-go v0.28.0 h1:7Rqx9M3ythTKy2J6uZLHmc8Sz9OGgIlseuO1iBX/s0M= +github.com/getsentry/sentry-go v0.28.0/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= +github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= +github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ= -github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v53 v53.2.0 h1:wvz3FyF53v4BK+AsnvCmeNhf8AkTaeh2SoYu/XUvTtI= github.com/google/go-github/v53 v53.2.0/go.mod h1:XhFRObz+m/l+UCm9b7KSIC3lT3NWSXGt7mOsAWEloao= -github.com/google/go-github/v55 v55.0.0 h1:4pp/1tNMB9X/LuAhs5i0KQAE40NmiR/y6prLNb9x9cg= -github.com/google/go-github/v55 v55.0.0/go.mod h1:JLahOTA1DnXzhxEymmFF5PP2tSS9JVNj68mSZNDwskA= +github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= +github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= +github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/k0kubun/pp/v3 v3.2.0 h1:h33hNTZ9nVFNP3u2Fsgz8JXiF5JINoZfFq4SvKJwNcs= github.com/k0kubun/pp/v3 v3.2.0/go.mod h1:ODtJQbQcIRfAD3N+theGCV1m/CBxweERz2dapdz1EwA= -github.com/k0kubun/sqldef v0.16.9 h1:8L9vF0gkZJi76LAQpzumpf+Ba9fCw6rjP7bjY8G9vPk= -github.com/k0kubun/sqldef v0.16.9/go.mod h1:dWnSMelL2dUGGV0/wRkhCHu51zarAqd6k+C5Ijo9wKQ= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/m-mizutani/bqs v0.0.6 h1:XsCbNzRd/sYiprrWVu6/Jfrda+TIAl5HiO9clBZZ9KI= +github.com/m-mizutani/bqs v0.0.6/go.mod h1:l2mEqmCWop3petWTgiGwYHWH39wYiNsO8KsD/VQO1C4= github.com/m-mizutani/clog v0.0.4 h1:6hY5CzHwNS4zuJhF6puazYPtGeaEEGIbrD4Ccimyaow= github.com/m-mizutani/clog v0.0.4/go.mod h1:a2J7BlnXOkaMQ0fNeDBG3IyyyWnCnSKYH8ltHFNDcHE= -github.com/m-mizutani/goerr v0.1.11 h1:noTEk8jNOVl/ST/Qfn0q7lMA13/ygzyl1PxaD4hHti4= -github.com/m-mizutani/goerr v0.1.11/go.mod h1:64HHjaK/ZjCy3VMaqrcZvinirVZkIBUxU21ml3WgMU4= +github.com/m-mizutani/goerr v0.1.13 h1:ApBRJ6vUkKEpB6IJX8ov0E0EK1yk4SWltHlUQaCLLLY= +github.com/m-mizutani/goerr v0.1.13/go.mod h1:OoNepSLW5oF3dQWWZ3D2lVOTbzRsePvc6LrqhXcff5Y= github.com/m-mizutani/gots v0.0.0-20230529013424-0639119b2cdd h1:mmuUG300WqGOvGNIIK3ELod2LpZZBV94fEgXK36xM0o= github.com/m-mizutani/gots v0.0.0-20230529013424-0639119b2cdd/go.mod h1:MDYrqsaKL6z6djDZpy6admhX6GOOCvyST+c0VnLbT4w= -github.com/m-mizutani/gt v0.0.8-0.20231008050155-f6c8702e116a h1:gNaslj9q/KvYPnlr1i+U2I/uB85XmpHG0LXZJmFFcfY= -github.com/m-mizutani/gt v0.0.8-0.20231008050155-f6c8702e116a/go.mod h1:0MPYSfGBLmYjTduzADVmIqD58ELQ5IfBFiK/f0FmB3k= -github.com/m-mizutani/masq v0.1.5 h1:+ebFJ9gdIZdiNA0X3Cn8MAXgfH4Z6m0cLJE9UNcKQKk= -github.com/m-mizutani/masq v0.1.5/go.mod h1:42/bKhlCNIQjmh3KBypeuh6iOvxNfUIlrZD1i0amEoc= +github.com/m-mizutani/gt v0.0.10 h1:gJsRcZ0R0kcVAGeahwDAVBCDCwOA/tFw3N1/kh3DnAY= +github.com/m-mizutani/gt v0.0.10/go.mod h1:0MPYSfGBLmYjTduzADVmIqD58ELQ5IfBFiK/f0FmB3k= +github.com/m-mizutani/masq v0.1.8 h1:sXTOtdVpWNpi4H+FxA5vyvs5HzqMjjYqNkL1JNjvS10= +github.com/m-mizutani/masq v0.1.8/go.mod h1:H8jy743m5h+niZ1ByiZfPnLNnXzb7Khr/K59vT15f18= +github.com/m-mizutani/opac v0.2.0 h1:VZdC0zK4Ls3JtnO812EIqzbKL0KKE4Cvy/jAKOmJN1U= +github.com/m-mizutani/opac v0.2.0/go.mod h1:L3oBhXqkb3yjYX0s1/IdI+lvZAm4PwBeXnjpMOZoH5Q= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/pganalyze/pg_query_go/v4 v4.2.3 h1:cNLqyiVMasV7YGWyYV+fkXyHp32gDfXVNCqoHztEGNk= -github.com/pganalyze/pg_query_go/v4 v4.2.3/go.mod h1:aEkDNOXNM5j0YGzaAapwJ7LB3dLNj+bvbWcLv1hOVqA= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/open-policy-agent/opa v0.65.0 h1:wnEU0pEk80YjFi3yoDbFTMluyNssgPI4VJNJetD9a4U= +github.com/open-policy-agent/opa v0.65.0/go.mod h1:CNoLL44LuCH1Yot/zoeZXRKFylQtCJV+oGFiP2TeeEc= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8= +github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= -github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= -github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= -github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY= -github.com/spdx/tools-golang v0.5.3/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI= -github.com/sqlc-dev/pqtype v0.3.0 h1:b09TewZ3cSnO5+M1Kqq05y0+OjqIptxELaSayg7bmqk= -github.com/sqlc-dev/pqtype v0.3.0/go.mod h1:oyUjp5981ctiL9UYvj1bVvCKi8OXkCa0u645hce7CAs= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= -github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= +github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= +github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= +github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 h1:vS1Ao/R55RNV4O7TA2Qopok8yN+X0LIP6RVWLFkprck= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0/go.mod h1:BMsdeOxN04K0L5FNUBfjFdvwWGNe/rkmSwH4Aelu/X0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= +go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= +golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= +gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= +google.golang.org/api v0.183.0 h1:PNMeRDwo1pJdgNcFQ9GstuLe/noWKIc89pRWRLMvLwE= +google.golang.org/api v0.183.0/go.mod h1:q43adC5/pHoSZTx5h2mSmdF7NcyfW9JuDyIOJAgS9ZQ= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20240604185151-ef581f913117 h1:HCZ6DlkKtCDAtD8ForECsY3tKuaR+p4R3grlK80uCCc= +google.golang.org/genproto v0.0.0-20240604185151-ef581f913117/go.mod h1:lesfX/+9iA+3OdqeCpoDddJaNxVB1AB6tD7EfqMmprc= +google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU= +google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -192,4 +380,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/pkg/controller/cli/cli.go b/pkg/controller/cli/cli.go index 155d1a15..f8372385 100644 --- a/pkg/controller/cli/cli.go +++ b/pkg/controller/cli/cli.go @@ -1,8 +1,6 @@ package cli import ( - "github.com/m-mizutani/octovy/pkg/controller/cli/migrate" - "github.com/m-mizutani/octovy/pkg/controller/cli/scan" "github.com/m-mizutani/octovy/pkg/controller/cli/serve" "github.com/m-mizutani/octovy/pkg/utils" "github.com/urfave/cli/v2" @@ -53,8 +51,7 @@ func (x *CLI) Run(argv []string) error { }, Commands: []*cli.Command{ serve.New(), - scan.New(), - migrate.New(), + insertCommand(), }, Before: func(ctx *cli.Context) error { if err := utils.ReconfigureLogger(logFormat, logLevel, logOutput); err != nil { diff --git a/pkg/controller/cli/config/bq.go b/pkg/controller/cli/config/bq.go new file mode 100644 index 00000000..a5b5129f --- /dev/null +++ b/pkg/controller/cli/config/bq.go @@ -0,0 +1,90 @@ +package config + +import ( + "context" + "log/slog" + + "github.com/m-mizutani/goerr" + "github.com/m-mizutani/octovy/pkg/domain/interfaces" + "github.com/m-mizutani/octovy/pkg/domain/types" + "github.com/m-mizutani/octovy/pkg/infra/bq" + "github.com/urfave/cli/v2" + "google.golang.org/api/impersonate" + "google.golang.org/api/option" +) + +type BigQuery struct { + projectID types.GoogleProjectID + datasetID types.BQDatasetID + tableID types.BQTableID + impersonateServiceAccount string +} + +func (x *BigQuery) Flags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "bigquery-project-id", + Usage: "BigQuery project ID", + Category: "BigQuery", + Destination: (*string)(&x.projectID), + EnvVars: []string{"OCTOVY_BIGQUERY_PROJECT_ID"}, + }, + &cli.StringFlag{ + Name: "bigquery-dataset-id", + Usage: "BigQuery dataset ID", + Category: "BigQuery", + Destination: (*string)(&x.datasetID), + EnvVars: []string{"OCTOVY_BIGQUERY_DATASET_ID"}, + }, + &cli.StringFlag{ + Name: "bigquery-table-id", + Usage: "BigQuery table ID", + Category: "BigQuery", + Destination: (*string)(&x.tableID), + EnvVars: []string{"OCTOVY_BIGQUERY_TABLE_ID"}, + Value: "scans", + }, + &cli.StringFlag{ + Name: "bq-impersonate-service-account", + Usage: "Impersonate service account for BigQuery", + Destination: &x.impersonateServiceAccount, + EnvVars: []string{"OCTOVY_BIGQUERY_IMPERSONATE_SERVICE_ACCOUNT"}, + }, + } +} + +func (x *BigQuery) TableID() types.BQTableID { + return x.tableID +} + +func (x *BigQuery) LogValue() slog.Value { + return slog.GroupValue( + slog.Any("ProjectID", x.projectID), + slog.Any("DatasetID", x.datasetID), + slog.Any("TableID", x.tableID), + slog.Any("ImpersonateServiceAccount", x.impersonateServiceAccount), + ) +} + +func (x *BigQuery) NewClient(ctx context.Context) (interfaces.BigQuery, error) { + if x.projectID == "" && x.datasetID == "" { + return nil, nil + } + var options []option.ClientOption + if x.impersonateServiceAccount != "" { + ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{ + TargetPrincipal: x.impersonateServiceAccount, + Scopes: []string{ + "https://www.googleapis.com/auth/bigquery", + "https://www.googleapis.com/auth/cloud-platform", + }, + }) + if err != nil { + return nil, goerr.Wrap(err, "failed to create token source for impersonate") + } + + options = append(options, option.WithTokenSource(ts)) + } + + return bq.New(ctx, x.projectID, x.datasetID, options...) +} diff --git a/pkg/controller/cli/config/cs.go b/pkg/controller/cli/config/cs.go new file mode 100644 index 00000000..19ed7071 --- /dev/null +++ b/pkg/controller/cli/config/cs.go @@ -0,0 +1,54 @@ +package config + +import ( + "context" + "log/slog" + + "github.com/m-mizutani/octovy/pkg/domain/interfaces" + "github.com/m-mizutani/octovy/pkg/infra/cs" + "github.com/urfave/cli/v2" +) + +type CloudStorage struct { + bucket string + prefix string +} + +func (x *CloudStorage) Flags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "cloud-storage-bucket", + Usage: "Cloud Storage bucket name", + Category: "Cloud Storage", + Destination: &x.bucket, + EnvVars: []string{"OCTOVY_CLOUD_STORAGE_BUCKET"}, + }, + &cli.StringFlag{ + Name: "cloud-storage-prefix", + Usage: "Cloud Storage prefix", + Category: "Cloud Storage", + Destination: &x.prefix, + EnvVars: []string{"OCTOVY_CLOUD_STORAGE_PREFIX"}, + }, + } +} + +func (x *CloudStorage) LogValue() slog.Value { + return slog.GroupValue( + slog.Any("Bucket", x.bucket), + slog.Any("Prefix", x.prefix), + ) +} + +func (x *CloudStorage) NewClient(ctx context.Context) (interfaces.Storage, error) { + if x.bucket == "" { + return nil, nil + } + + var options []cs.Option + if x.prefix != "" { + options = append(options, cs.WithPrefix(x.prefix)) + } + + return cs.New(ctx, x.bucket, options...) +} diff --git a/pkg/controller/cli/config/db.go b/pkg/controller/cli/config/db.go deleted file mode 100644 index 5d8a3397..00000000 --- a/pkg/controller/cli/config/db.go +++ /dev/null @@ -1,173 +0,0 @@ -package config - -import ( - "context" - "database/sql" - "errors" - "fmt" - "log/slog" - "os" - "strings" - "time" - - "github.com/k0kubun/sqldef" - "github.com/k0kubun/sqldef/database" - "github.com/k0kubun/sqldef/database/postgres" - "github.com/k0kubun/sqldef/schema" - "github.com/m-mizutani/goerr" - "github.com/m-mizutani/octovy/pkg/utils" - "github.com/urfave/cli/v2" - - db "github.com/m-mizutani/octovy/database" -) - -type DB struct { - User string - Password string `masq:"secret"` - Host string - Port int - DBName string - SSLMode string -} - -func (x *DB) Flags() []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: "db-user", - Category: "Database", - Usage: "database user", - EnvVars: []string{"OCTOVY_DB_USER"}, - Required: true, - Destination: &x.User, - }, - &cli.StringFlag{ - Name: "db-password", - Category: "Database", - Usage: "database password", - EnvVars: []string{"OCTOVY_DB_PASSWORD"}, - Destination: &x.Password, - }, - &cli.StringFlag{ - Name: "db-host", - Category: "Database", - Usage: "database host", - EnvVars: []string{"OCTOVY_DB_HOST"}, - Destination: &x.Host, - Value: "localhost", - }, - &cli.IntFlag{ - Name: "db-port", - Category: "Database", - Usage: "database port", - EnvVars: []string{"OCTOVY_DB_PORT"}, - Destination: &x.Port, - Value: 5432, - }, - &cli.StringFlag{ - Name: "db-name", - Category: "Database", - Usage: "database name", - EnvVars: []string{"OCTOVY_DB_NAME"}, - Required: true, - Destination: &x.DBName, - }, - &cli.StringFlag{ - Name: "db-ssl-mode", - Category: "Database", - Usage: "database SSL mode", - EnvVars: []string{"OCTOVY_DB_SSL_MODE"}, - Destination: &x.SSLMode, - Value: "disable", - }, - } -} - -func (x *DB) DSN() string { - var options []string - if x.User != "" { - options = append(options, "user="+x.User) - } - if x.Password != "" { - options = append(options, "password="+x.Password) - } - if x.Host != "" { - options = append(options, "host="+x.Host) - } - if x.Port != 0 { - options = append(options, "port="+fmt.Sprintf("%d", x.Port)) - } - if x.DBName != "" { - options = append(options, "dbname="+x.DBName) - } - if x.SSLMode != "" { - options = append(options, "sslmode="+x.SSLMode) - } - - return strings.Join(options, " ") -} - -func (x *DB) Connect(ctx context.Context) (*sql.DB, error) { - dbClient, err := sql.Open("postgres", x.DSN()) - if err != nil { - return nil, goerr.Wrap(err, "failed to open database") - } - - ctx, cancel := context.WithDeadline(ctx, time.Now().Add(20*time.Second)) - defer cancel() - - if err := waitDBReady(ctx, dbClient); err != nil { - return nil, err - } - - return dbClient, nil -} - -func waitDBReady(ctx context.Context, db *sql.DB) error { - utils.Logger().Info("waiting database ready") - for { - err := db.PingContext(ctx) - if err == nil { - utils.Logger().Info("database is ready") - return nil - } else if errors.Is(err, context.DeadlineExceeded) { - return goerr.Wrap(err, "database is not ready, and timeout") - } - utils.Logger().Warn("database is not ready", slog.Any("error", err)) - time.Sleep(1 * time.Second) - } -} - -func (x *DB) Migrate(dryRun bool) error { - utils.Logger().Info("migrating database", slog.Any("config.DB", x)) - - options := &sqldef.Options{ - DesiredDDLs: db.Schema(), - DryRun: dryRun, - Export: false, - EnableDropTable: true, - // BeforeApply: opts.BeforeApply, - // Config: database.ParseGeneratorConfig(opts.Config), - } - - config := database.Config{ - DbName: x.DBName, - User: x.User, - Password: x.Password, - Host: x.Host, - Port: x.Port, - } - if err := os.Setenv("PGSSLMODE", x.SSLMode); err != nil { - return goerr.Wrap(err, "failed to set PGSSLMODE") - } - - db, err := postgres.NewDatabase(config) - if err != nil { - return goerr.Wrap(err, "failed to open database") - } - defer utils.SafeClose(db) - - sqlParser := postgres.NewParser() - sqldef.Run(schema.GeneratorModePostgres, db, sqlParser, options) - - return nil -} diff --git a/pkg/controller/cli/config/github_app.go b/pkg/controller/cli/config/github_app.go index 76f5dc32..e5bbbc71 100644 --- a/pkg/controller/cli/config/github_app.go +++ b/pkg/controller/cli/config/github_app.go @@ -2,6 +2,7 @@ package config import ( "encoding/base64" + "log/slog" "github.com/m-mizutani/octovy/pkg/domain/types" "github.com/urfave/cli/v2" @@ -23,14 +24,6 @@ func (x *GitHubApp) Flags() []cli.Flag { EnvVars: []string{"OCTOVY_GITHUB_APP_ID"}, Required: true, }, - &cli.StringFlag{ - Name: "github-app-secret", - Usage: "GitHub App Secret", - Category: "GitHub App", - Destination: (*string)(&x.Secret), - EnvVars: []string{"OCTOVY_GITHUB_APP_SECRET"}, - Required: true, - }, &cli.StringFlag{ Name: "github-app-private-key", Usage: "GitHub App Private Key", @@ -39,6 +32,13 @@ func (x *GitHubApp) Flags() []cli.Flag { EnvVars: []string{"OCTOVY_GITHUB_APP_PRIVATE_KEY"}, Required: true, }, + &cli.StringFlag{ + Name: "github-app-secret", + Usage: "GitHub App Webhook Secret", + Category: "GitHub App", + Destination: (*string)(&x.Secret), + EnvVars: []string{"OCTOVY_GITHUB_APP_SECRET"}, + }, } } @@ -48,3 +48,11 @@ func (x *GitHubApp) PrivateKey() types.GitHubAppPrivateKey { } return x.privateKey } + +func (x *GitHubApp) LogValue() slog.Value { + return slog.GroupValue( + slog.Any("ID", x.ID), + slog.Any("Secret.len", len(x.Secret)), + slog.Any("PrivateKey.len", len(x.privateKey)), + ) +} diff --git a/pkg/controller/cli/config/policy.go b/pkg/controller/cli/config/policy.go new file mode 100644 index 00000000..950c7c35 --- /dev/null +++ b/pkg/controller/cli/config/policy.go @@ -0,0 +1,35 @@ +package config + +import ( + "github.com/m-mizutani/goerr" + "github.com/m-mizutani/opac" + "github.com/urfave/cli/v2" +) + +type Policy struct { + files cli.StringSlice +} + +func (x *Policy) Flags() []cli.Flag { + return []cli.Flag{ + &cli.StringSliceFlag{ + Name: "policy-file", + Usage: "Policy files to evaluate", + EnvVars: []string{"OCTOVY_POLICY_FILE"}, + Destination: &x.files, + }, + } +} + +func (x *Policy) Configure() (*opac.Client, error) { + if len(x.files.Value()) == 0 { + return nil, nil + } + + client, err := opac.New(opac.Files(x.files.Value()...)) + if err != nil { + return nil, goerr.Wrap(err, "Failed to initialize policy engine").With("files", x.files.Value()) + } + + return client, nil +} diff --git a/pkg/controller/cli/config/sentry.go b/pkg/controller/cli/config/sentry.go new file mode 100644 index 00000000..40db086b --- /dev/null +++ b/pkg/controller/cli/config/sentry.go @@ -0,0 +1,55 @@ +package config + +import ( + "log/slog" + + "github.com/getsentry/sentry-go" + "github.com/m-mizutani/goerr" + "github.com/m-mizutani/octovy/pkg/utils" + "github.com/urfave/cli/v2" +) + +type Sentry struct { + dsn string + env string +} + +func (x *Sentry) Flags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "sentry-dsn", + Usage: "Sentry DSN for error reporting", + EnvVars: []string{"OCTOVY_SENTRY_DSN"}, + Destination: &x.dsn, + }, + &cli.StringFlag{ + Name: "sentry-env", + Usage: "Sentry environment", + EnvVars: []string{"OCTOVY_SENTRY_ENV"}, + Destination: &x.env, + }, + } +} + +func (x *Sentry) Configure() error { + if x.dsn != "" { + utils.Logger().Info("Enable Sentry", "DSN", x.dsn, "env", x.env) + if err := sentry.Init(sentry.ClientOptions{ + Dsn: x.dsn, + Environment: x.env, + }); err != nil { + return goerr.Wrap(err, "failed to initialize Sentry") + } + } else { + utils.Logger().Warn("sentry is not enabled") + } + + return nil +} + +func (x *Sentry) LogValue() slog.Value { + return slog.GroupValue( + slog.String("dsn", x.dsn), + slog.String("env", x.env), + ) +} diff --git a/pkg/controller/cli/insert.go b/pkg/controller/cli/insert.go new file mode 100644 index 00000000..f165cc9f --- /dev/null +++ b/pkg/controller/cli/insert.go @@ -0,0 +1,97 @@ +package cli + +import ( + "encoding/json" + "io" + "os" + "path/filepath" + + "github.com/m-mizutani/goerr" + "github.com/m-mizutani/gots/slice" + "github.com/m-mizutani/octovy/pkg/controller/cli/config" + "github.com/m-mizutani/octovy/pkg/domain/model" + "github.com/m-mizutani/octovy/pkg/domain/model/trivy" + "github.com/m-mizutani/octovy/pkg/infra" + "github.com/m-mizutani/octovy/pkg/usecase" + "github.com/urfave/cli/v2" +) + +func insertCommand() *cli.Command { + var ( + bigQuery config.BigQuery + filePath string + meta model.GitHubMetadata + ) + + return &cli.Command{ + Name: "insert", + Aliases: []string{"i"}, + Usage: "Insert trivy scan result JSON file to BigQuery", + Flags: slice.Flatten([]cli.Flag{ + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: "Path to JSON file generated by trivy. '-' is stdin", + Value: "-", + Destination: &filePath, + }, + + &cli.StringFlag{ + Name: "github-owner", + Usage: "GitHub repository owner", + EnvVars: []string{"OCTOVY_GITHUB_OWNER"}, + Destination: &meta.Owner, + Required: true, + }, + &cli.StringFlag{ + Name: "github-repo", + Usage: "GitHub repository name", + EnvVars: []string{"OCTOVY_GITHUB_REPO"}, + Destination: &meta.RepoName, + Required: true, + }, + &cli.StringFlag{ + Name: "github-commit-id", + Usage: "GitHub commit ID", + EnvVars: []string{"OCTOVY_GITHUB_COMMIT_ID"}, + Destination: &meta.CommitID, + Required: true, + }, + }, bigQuery.Flags()), + Action: func(c *cli.Context) error { + ctx := c.Context + + bqClient, err := bigQuery.NewClient(ctx) + if err != nil { + return err + } + + clients := infra.New(infra.WithBigQuery(bqClient)) + uc := usecase.New(clients) + + var r io.Reader + switch filePath { + case "-": + r = c.App.Reader + default: + f, err := os.Open(filepath.Clean(filePath)) + if err != nil { + return goerr.Wrap(err).With("file", filePath) + } + defer f.Close() + r = f + } + + var report trivy.Report + if err := json.NewDecoder(r).Decode(&report); err != nil { + return goerr.Wrap(err, "failed to parse trivy result data") + } + + if err := uc.InsertScanResult(ctx, meta, report); err != nil { + return goerr.Wrap(err).With("file", filePath) + } + + return nil + }, + } +} diff --git a/pkg/controller/cli/migrate/migrate.go b/pkg/controller/cli/migrate/migrate.go deleted file mode 100644 index 8adf7e2a..00000000 --- a/pkg/controller/cli/migrate/migrate.go +++ /dev/null @@ -1,36 +0,0 @@ -package migrate - -import ( - "github.com/m-mizutani/gots/slice" - "github.com/urfave/cli/v2" - - "github.com/m-mizutani/octovy/pkg/controller/cli/config" -) - -func New() *cli.Command { - var ( - dryRun bool - dbConfig config.DB - ) - - baseFlags := []cli.Flag{ - &cli.BoolFlag{ - Name: "dry-run", - Usage: "Dry run mode", - Destination: &dryRun, - }, - } - - return &cli.Command{ - Name: "migrate", - Aliases: []string{"m"}, - Usage: "Migrate database", - Flags: slice.Flatten( - baseFlags, - dbConfig.Flags(), - ), - Action: func(c *cli.Context) error { - return dbConfig.Migrate(dryRun) - }, - } -} diff --git a/pkg/controller/cli/scan/cli.go b/pkg/controller/cli/scan/cli.go deleted file mode 100644 index 19640338..00000000 --- a/pkg/controller/cli/scan/cli.go +++ /dev/null @@ -1,26 +0,0 @@ -package scan - -import ( - "github.com/urfave/cli/v2" -) - -func New() *cli.Command { - var ( - dir string - ) - return &cli.Command{ - Name: "scan", - Usage: "Local scan mode", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "dir", - Usage: "Target directory", - Value: ".", - Destination: &dir, - }, - }, - Action: func(ctx *cli.Context) error { - return nil - }, - } -} diff --git a/pkg/controller/cli/serve/serve.go b/pkg/controller/cli/serve/serve.go index 5cd472a5..8a501eca 100644 --- a/pkg/controller/cli/serve/serve.go +++ b/pkg/controller/cli/serve/serve.go @@ -29,10 +29,11 @@ func New() *cli.Command { addr string trivyPath string - skipMigration bool - - githubApp config.GitHubApp - database config.DB + githubApp config.GitHubApp + bigQuery config.BigQuery + cloudStorage config.CloudStorage + sentry config.Sentry + policy config.Policy ) serveFlags := []cli.Flag{ &cli.StringFlag{ @@ -49,11 +50,6 @@ func New() *cli.Command { EnvVars: []string{"OCTOVY_TRIVY_PATH"}, Destination: &trivyPath, }, - &cli.BoolFlag{ - Name: "skip-migration", - Usage: "Skip database migration", - Destination: &skipMigration, - }, } return &cli.Command{ @@ -63,26 +59,24 @@ func New() *cli.Command { Flags: slice.Flatten( serveFlags, githubApp.Flags(), - database.Flags(), + bigQuery.Flags(), + cloudStorage.Flags(), + sentry.Flags(), + policy.Flags(), ), Action: func(c *cli.Context) error { utils.Logger().Info("starting serve", - slog.Any("addr", addr), - slog.Any("trivyPath", trivyPath), - slog.Any("githubApp", githubApp), - slog.Any("database", database), + slog.Any("Addr", addr), + slog.Any("TrivyPath", trivyPath), + slog.Any("GitHubApp", githubApp), + slog.Any("BigQuery", bigQuery), + slog.Any("CloudStorage", cloudStorage), + slog.Any("Sentry", sentry), + slog.Any("Policy", policy), ) - dbClient, err := database.Connect(c.Context) - if err != nil { - return goerr.Wrap(err, "failed to open database") - } - defer utils.SafeClose(dbClient) - - if !skipMigration { - if err := database.Migrate(false); err != nil { - return err - } + if err := sentry.Configure(); err != nil { + return err } ghApp, err := gh.New(githubApp.ID, githubApp.PrivateKey()) @@ -90,13 +84,33 @@ func New() *cli.Command { return err } - clients := infra.New( + infraOptions := []infra.Option{ infra.WithGitHubApp(ghApp), infra.WithTrivy(trivy.New(trivyPath)), - infra.WithDB(dbClient), - ) + } + + if bqClient, err := bigQuery.NewClient(c.Context); err != nil { + return err + } else if bqClient != nil { + infraOptions = append(infraOptions, infra.WithBigQuery(bqClient)) + } + + if csClient, err := cloudStorage.NewClient(c.Context); err != nil { + return err + } else if csClient != nil { + infraOptions = append(infraOptions, infra.WithStorage(csClient)) + } + + if policyClient, err := policy.Configure(); err != nil { + return err + } else if policyClient != nil { + infraOptions = append(infraOptions, infra.WithPolicy(policyClient)) + } + + clients := infra.New(infraOptions...) + uc := usecase.New(clients) - s := server.New(uc, githubApp.Secret) + s := server.New(uc, server.WithGitHubSecret(githubApp.Secret)) serverErr := make(chan error, 1) httpServer := &http.Server{ diff --git a/pkg/controller/server/github.go b/pkg/controller/server/github.go index b216f522..e3b23454 100644 --- a/pkg/controller/server/github.go +++ b/pkg/controller/server/github.go @@ -8,13 +8,14 @@ import ( "github.com/google/go-github/v53/github" "github.com/m-mizutani/goerr" + "github.com/m-mizutani/octovy/pkg/domain/interfaces" "github.com/m-mizutani/octovy/pkg/domain/model" "github.com/m-mizutani/octovy/pkg/domain/types" - "github.com/m-mizutani/octovy/pkg/usecase" "github.com/m-mizutani/octovy/pkg/utils" ) -func handleGitHubEvent(uc usecase.UseCase, r *http.Request, key types.GitHubAppSecret) error { +func handleGitHubAppEvent(uc interfaces.UseCase, r *http.Request, key types.GitHubAppSecret) error { + ctx := r.Context() payload, err := github.ValidatePayload(r, []byte(key)) if err != nil { return goerr.Wrap(err, "validating payload") @@ -25,13 +26,7 @@ func handleGitHubEvent(uc usecase.UseCase, r *http.Request, key types.GitHubAppS return goerr.Wrap(err, "parsing webhook") } - ctx := r.Context() - var octovyCtx *model.Context - if v, ok := ctx.(*model.Context); !ok { - octovyCtx = model.NewContext(model.WithBase(ctx)) - } else { - octovyCtx = v - } + utils.CtxLogger(ctx).With(slog.Any("event", event)).Info("Received GitHub App event") scanInput := githubEventToScanInput(event) if scanInput == nil { @@ -39,28 +34,26 @@ func handleGitHubEvent(uc usecase.UseCase, r *http.Request, key types.GitHubAppS } utils.Logger().With(slog.Any("input", scanInput)).Info("Scan GitHub repository") - if err := uc.ScanGitHubRepo(octovyCtx, scanInput); err != nil { - return err + + if err := uc.ScanGitHubRepo(r.Context(), scanInput); err != nil { + return goerr.Wrap(err, "failed to scan GitHub repository") } return nil } func refToBranch(v string) string { - if ref := strings.SplitN(v, "/", 3); len(ref) == 3 && ref[1] == "heads" { + if ref := strings.SplitN(v, "/", 3); len(ref) == 3 && ref[0] == "refs" && ref[1] == "heads" { return ref[2] } return v } -func githubEventToScanInput(event interface{}) *usecase.ScanGitHubRepoInput { +func githubEventToScanInput(event interface{}) *model.ScanGitHubRepoInput { switch ev := event.(type) { case *github.PushEvent: - branch := refToBranch(ev.GetRef()) - isDefaultBranch := branch == ev.GetRepo().GetDefaultBranch() - - return &usecase.ScanGitHubRepoInput{ - GitHubRepoMetadata: usecase.GitHubRepoMetadata{ + return &model.ScanGitHubRepoInput{ + GitHubMetadata: model.GitHubMetadata{ GitHubCommit: model.GitHubCommit{ GitHubRepo: model.GitHubRepo{ RepoID: ev.GetRepo().GetID(), @@ -68,11 +61,14 @@ func githubEventToScanInput(event interface{}) *usecase.ScanGitHubRepoInput { RepoName: ev.GetRepo().GetName(), }, CommitID: ev.GetHeadCommit().GetID(), + Branch: refToBranch(ev.GetRef()), + Ref: ev.GetRef(), + Committer: model.GitHubUser{ + Login: ev.GetHeadCommit().GetCommitter().GetLogin(), + Email: ev.GetHeadCommit().GetCommitter().GetEmail(), + }, }, - Branch: branch, - BaseCommitID: ev.GetBefore(), - PullRequestID: 0, - IsDefaultBranch: isDefaultBranch, + DefaultBranch: ev.GetRepo().GetDefaultBranch(), }, InstallID: types.GitHubAppInstallID(ev.GetInstallation().GetID()), } @@ -82,30 +78,48 @@ func githubEventToScanInput(event interface{}) *usecase.ScanGitHubRepoInput { utils.Logger().Debug("ignore PR event", slog.String("action", ev.GetAction())) return nil } - - branch := refToBranch(ev.GetPullRequest().GetHead().GetRef()) - baseCommitID := ev.GetBefore() - if baseCommitID == "" { - baseCommitID = ev.GetPullRequest().GetBase().GetSHA() + if ev.GetPullRequest().GetDraft() { + utils.Logger().Debug("ignore draft PR", slog.String("action", ev.GetAction())) + return nil } - return &usecase.ScanGitHubRepoInput{ - GitHubRepoMetadata: usecase.GitHubRepoMetadata{ + pr := ev.GetPullRequest() + + input := &model.ScanGitHubRepoInput{ + GitHubMetadata: model.GitHubMetadata{ GitHubCommit: model.GitHubCommit{ GitHubRepo: model.GitHubRepo{ RepoID: ev.GetRepo().GetID(), Owner: ev.GetRepo().GetOwner().GetLogin(), RepoName: ev.GetRepo().GetName(), }, - CommitID: ev.GetPullRequest().GetHead().GetSHA(), + CommitID: pr.GetHead().GetSHA(), + Ref: pr.GetHead().GetRef(), + Branch: pr.GetHead().GetRef(), + Committer: model.GitHubUser{ + ID: pr.GetHead().GetUser().GetID(), + Login: pr.GetHead().GetUser().GetLogin(), + Email: pr.GetHead().GetUser().GetEmail(), + }, + }, + DefaultBranch: ev.GetRepo().GetDefaultBranch(), + PullRequest: &model.GitHubPullRequest{ + ID: pr.GetID(), + Number: pr.GetNumber(), + BaseBranch: pr.GetBase().GetRef(), + BaseCommitID: pr.GetBase().GetSHA(), + User: model.GitHubUser{ + ID: pr.GetBase().GetUser().GetID(), + Login: pr.GetBase().GetUser().GetLogin(), + Email: pr.GetBase().GetUser().GetEmail(), + }, }, - Branch: branch, - BaseCommitID: baseCommitID, - PullRequestID: ev.GetPullRequest().GetNumber(), }, InstallID: types.GitHubAppInstallID(ev.GetInstallation().GetID()), } + return input + case *github.InstallationEvent, *github.InstallationRepositoriesEvent: return nil // ignore @@ -114,3 +128,7 @@ func githubEventToScanInput(event interface{}) *usecase.ScanGitHubRepoInput { return nil } } + +func handleGitHubActionEvent(_ interfaces.UseCase, _ *http.Request) error { + return nil +} diff --git a/pkg/controller/server/github_test.go b/pkg/controller/server/github_test.go index a1c6d0b4..6c7e598a 100644 --- a/pkg/controller/server/github_test.go +++ b/pkg/controller/server/github_test.go @@ -2,6 +2,7 @@ package server_test import ( "bytes" + "context" "crypto/hmac" "crypto/sha256" _ "embed" @@ -24,6 +25,9 @@ var testGitHubPullRequestOpened []byte //go:embed testdata/github/pull_request.synchronize.json var testGitHubPullRequestSynchronize []byte +//go:embed testdata/github/pull_request.synchronize-draft.json +var testGitHubPullRequestSynchronizeDraft []byte + //go:embed testdata/github/push.json var testGitHubPush []byte @@ -33,121 +37,165 @@ var testGitHubPushDefault []byte func TestGitHubPullRequestSync(t *testing.T) { const secret = "dummy" - testCases := map[string]struct { + type testCase struct { + input *model.ScanGitHubRepoInput event string body []byte - input usecase.ScanGitHubRepoInput - }{ - "pull_request.opened": { - event: "pull_request", - body: testGitHubPullRequestOpened, - input: usecase.ScanGitHubRepoInput{ - GitHubRepoMetadata: usecase.GitHubRepoMetadata{ - GitHubCommit: model.GitHubCommit{ - GitHubRepo: model.GitHubRepo{ - RepoID: 581995051, - Owner: "m-mizutani", - RepoName: "masq", - }, - CommitID: "aa0378cad00d375c1897c1b5b5a4dd125984b511", + } + + runTest := func(tc testCase) func(t *testing.T) { + return func(t *testing.T) { + var called int + mock := &usecase.Mock{ + MockScanGitHubRepo: func(ctx context.Context, input *model.ScanGitHubRepoInput) error { + called++ + gt.V(t, input).Equal(tc.input) + return nil + }, + } + + serv := server.New(mock, server.WithGitHubSecret(secret)) + req := newGitHubWebhookRequest(t, tc.event, tc.body, secret) + w := httptest.NewRecorder() + serv.Mux().ServeHTTP(w, req) + gt.V(t, w.Code).Equal(http.StatusOK) + if tc.input != nil { + gt.V(t, called).Equal(1) + } else { + gt.V(t, called).Equal(0) + } + } + } + + t.Run("pull_request.opened", runTest(testCase{ + event: "pull_request", + body: testGitHubPullRequestOpened, + input: &model.ScanGitHubRepoInput{ + GitHubMetadata: model.GitHubMetadata{ + GitHubCommit: model.GitHubCommit{ + GitHubRepo: model.GitHubRepo{ + RepoID: 581995051, + Owner: "m-mizutani", + RepoName: "masq", + }, + Ref: "update/packages/20230918", + Branch: "update/packages/20230918", + CommitID: "aa0378cad00d375c1897c1b5b5a4dd125984b511", + Committer: model.GitHubUser{ + ID: 605953, + Login: "m-mizutani", }, - PullRequestID: 13, - Branch: "update/packages/20230918", - BaseCommitID: "8acdc26c9f12b9cc88e5f0b23f082f648d9e5645", - IsDefaultBranch: false, }, - InstallID: 41633205, - }, - }, - "pull_request.synchronize": { - event: "pull_request", - body: testGitHubPullRequestSynchronize, - input: usecase.ScanGitHubRepoInput{ - GitHubRepoMetadata: usecase.GitHubRepoMetadata{ - GitHubCommit: model.GitHubCommit{ - GitHubRepo: model.GitHubRepo{ - RepoID: 359010704, - Owner: "m-mizutani", - RepoName: "octovy", - }, - CommitID: "69454c171c2f0f2dbc9ccb0c9ef9b72fd769f046", + DefaultBranch: "main", + PullRequest: &model.GitHubPullRequest{ + ID: 1518635674, + Number: 13, + BaseBranch: "main", + BaseCommitID: "8acdc26c9f12b9cc88e5f0b23f082f648d9e5645", + User: model.GitHubUser{ + ID: 605953, + Login: "m-mizutani", }, - PullRequestID: 89, - Branch: "release/v0.2.0", - BaseCommitID: "bca5ddd2023d5c906a0420492deb2ede8d99eb79", - IsDefaultBranch: false, }, - InstallID: 41633205, }, + InstallID: 41633205, }, + })) - "push": { - event: "push", - body: testGitHubPush, - input: usecase.ScanGitHubRepoInput{ - GitHubRepoMetadata: usecase.GitHubRepoMetadata{ - GitHubCommit: model.GitHubCommit{ - GitHubRepo: model.GitHubRepo{ - RepoID: 581995051, - Owner: "m-mizutani", - RepoName: "masq", - }, - CommitID: "aa0378cad00d375c1897c1b5b5a4dd125984b511", + t.Run("pull_request.synchronize", runTest(testCase{ + event: "pull_request", + body: testGitHubPullRequestSynchronize, + input: &model.ScanGitHubRepoInput{ + GitHubMetadata: model.GitHubMetadata{ + GitHubCommit: model.GitHubCommit{ + GitHubRepo: model.GitHubRepo{ + RepoID: 359010704, + Owner: "m-mizutani", + RepoName: "octovy", + }, + Ref: "release/v0.2.0", + Branch: "release/v0.2.0", + CommitID: "69454c171c2f0f2dbc9ccb0c9ef9b72fd769f046", + Committer: model.GitHubUser{ + ID: 605953, + Login: "m-mizutani", + }, + }, + DefaultBranch: "main", + PullRequest: &model.GitHubPullRequest{ + ID: 1473604329, + Number: 89, + BaseCommitID: "08fb7816c6d0a485239ca5f342342186f972a6e7", + BaseBranch: "main", + User: model.GitHubUser{ + ID: 605953, + Login: "m-mizutani", }, - PullRequestID: 0, - Branch: "update/packages/20230918", - BaseCommitID: "0000000000000000000000000000000000000000", - IsDefaultBranch: false, }, - InstallID: 41633205, }, + InstallID: 41633205, }, - "push: to default": { - event: "push", - body: testGitHubPushDefault, - input: usecase.ScanGitHubRepoInput{ - GitHubRepoMetadata: usecase.GitHubRepoMetadata{ - GitHubCommit: model.GitHubCommit{ - GitHubRepo: model.GitHubRepo{ - RepoID: 281879096, - Owner: "m-mizutani", - RepoName: "ops", - }, - CommitID: "f58ae7668c3dfc193a1d2c0372cc52847613cde4", + })) + + t.Run("pull_request.synchronize: draft", runTest(testCase{ + event: "pull_request", + body: testGitHubPullRequestSynchronizeDraft, + input: nil, + })) + + t.Run("push", runTest(testCase{ + event: "push", + body: testGitHubPush, + input: &model.ScanGitHubRepoInput{ + GitHubMetadata: model.GitHubMetadata{ + GitHubCommit: model.GitHubCommit{ + GitHubRepo: model.GitHubRepo{ + RepoID: 581995051, + Owner: "m-mizutani", + RepoName: "masq", + }, + CommitID: "aa0378cad00d375c1897c1b5b5a4dd125984b511", + Ref: "refs/heads/update/packages/20230918", + Branch: "update/packages/20230918", + Committer: model.GitHubUser{ + Login: "m-mizutani", + Email: "mizutani@hey.com", }, - PullRequestID: 0, - Branch: "master", - BaseCommitID: "987e1005c2e3c79631b620c4a76afd4b8111b7b1", - IsDefaultBranch: true, }, - InstallID: 41633205, + DefaultBranch: "main", }, + InstallID: 41633205, }, - } + })) - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - var called int - mock := &usecase.Mock{ - MockScanGitHubRepo: func(ctx *model.Context, input *usecase.ScanGitHubRepoInput) error { - called++ - gt.V(t, input).Equal(&tc.input) - return nil + t.Run("push: to default", runTest(testCase{ + event: "push", + body: testGitHubPushDefault, + input: &model.ScanGitHubRepoInput{ + GitHubMetadata: model.GitHubMetadata{ + GitHubCommit: model.GitHubCommit{ + GitHubRepo: model.GitHubRepo{ + RepoID: 281879096, + Owner: "m-mizutani", + RepoName: "ops", + }, + CommitID: "f58ae7668c3dfc193a1d2c0372cc52847613cde4", + Ref: "refs/heads/master", + Branch: "master", + Committer: model.GitHubUser{ + Login: "m-mizutani", + Email: "mizutani@hey.com", + }, }, - } - - serv := server.New(mock, secret) - req := newGitHubWebhookRequest(t, tc.event, tc.body, secret) - w := httptest.NewRecorder() - serv.Mux().ServeHTTP(w, req) - gt.V(t, w.Code).Equal(http.StatusOK) - gt.V(t, called).Equal(1) - }) - } + DefaultBranch: "master", + }, + InstallID: 41633205, + }, + })) } func newGitHubWebhookRequest(t *testing.T, event string, body []byte, secret types.GitHubAppSecret) *http.Request { - req := gt.R1(http.NewRequest(http.MethodPost, "/webhook/github", bytes.NewReader(body))).NoError(t) + req := gt.R1(http.NewRequest(http.MethodPost, "/webhook/github/app", bytes.NewReader(body))).NoError(t) h := hmac.New(sha256.New, []byte(secret)) h.Write(body) diff --git a/pkg/controller/server/middleware.go b/pkg/controller/server/middleware.go index e5d4a8d8..e692da39 100644 --- a/pkg/controller/server/middleware.go +++ b/pkg/controller/server/middleware.go @@ -7,7 +7,6 @@ import ( "log/slog" "github.com/google/uuid" - "github.com/m-mizutani/octovy/pkg/domain/model" "github.com/m-mizutani/octovy/pkg/utils" ) @@ -15,10 +14,7 @@ func preProcess(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logger := utils.Logger().With(slog.String("request_id", uuid.NewString())) - ctx := model.NewContext( - model.WithLogger(logger), - model.WithBase(r.Context()), - ) + ctx := utils.CtxWithLogger(r.Context(), logger) lw := &statusCodeLogger{ResponseWriter: w} diff --git a/pkg/controller/server/server.go b/pkg/controller/server/server.go index d8db2e0f..1f4e1130 100644 --- a/pkg/controller/server/server.go +++ b/pkg/controller/server/server.go @@ -6,8 +6,8 @@ import ( "log/slog" "github.com/go-chi/chi/v5" + "github.com/m-mizutani/octovy/pkg/domain/interfaces" "github.com/m-mizutani/octovy/pkg/domain/types" - "github.com/m-mizutani/octovy/pkg/usecase" "github.com/m-mizutani/octovy/pkg/utils" ) @@ -17,26 +17,57 @@ type Server struct { func safeWrite(w http.ResponseWriter, code int, body []byte) { w.WriteHeader(code) + + // nosemgrep: go.lang.security.audit.xss.no-direct-write-to-responsewriter.no-direct-write-to-responsewriter + // Why: The response data is not from user input if _, err := w.Write(body); err != nil { utils.Logger().Error("fail to write response", slog.Any("error", err)) } } -func New(uc usecase.UseCase, secret types.GitHubAppSecret) *Server { +type config struct { + ghSecret types.GitHubAppSecret +} + +type Option func(*config) + +func WithGitHubSecret(secret types.GitHubAppSecret) Option { + return func(cfg *config) { + cfg.ghSecret = secret + } +} + +func New(uc interfaces.UseCase, options ...Option) *Server { + cfg := &config{} + for _, opt := range options { + opt(cfg) + } + r := chi.NewRouter() r.Use(preProcess) r.Get("/health", func(w http.ResponseWriter, r *http.Request) { safeWrite(w, http.StatusOK, []byte("ok")) }) r.Route("/webhook", func(r chi.Router) { - r.Post("/github", func(w http.ResponseWriter, r *http.Request) { - if err := handleGitHubEvent(uc, r, secret); err != nil { - utils.Logger().Warn("fail to handle GitHub event", slog.Any("error", err)) - safeWrite(w, http.StatusInternalServerError, []byte(err.Error())) - return - } - - safeWrite(w, http.StatusOK, []byte("ok")) + r.Route("/github", func(r chi.Router) { + r.Post("/app", func(w http.ResponseWriter, r *http.Request) { + if err := handleGitHubAppEvent(uc, r, cfg.ghSecret); err != nil { + utils.HandleError(r.Context(), "fail to handle GitHub App event", err) + safeWrite(w, http.StatusInternalServerError, []byte(err.Error())) + return + } + + safeWrite(w, http.StatusOK, []byte("ok")) + }) + r.Post("/action", func(w http.ResponseWriter, r *http.Request) { + if err := handleGitHubActionEvent(uc, r); err != nil { + utils.HandleError(r.Context(), "fail to handle GitHub action event", err) + safeWrite(w, http.StatusInternalServerError, []byte(err.Error())) + return + } + + safeWrite(w, http.StatusOK, []byte("ok")) + }) }) }) diff --git a/pkg/controller/server/testdata/github/pull_request.synchronize-draft.json b/pkg/controller/server/testdata/github/pull_request.synchronize-draft.json new file mode 100644 index 00000000..fe7fd3fa --- /dev/null +++ b/pkg/controller/server/testdata/github/pull_request.synchronize-draft.json @@ -0,0 +1,530 @@ +{ + "action": "synchronize", + "number": 89, + "pull_request": { + "url": "https://api.github.com/repos/m-mizutani/octovy/pulls/89", + "id": 1473604329, + "node_id": "PR_kwDOFWYRkM5X1Wrp", + "html_url": "https://github.com/m-mizutani/octovy/pull/89", + "diff_url": "https://github.com/m-mizutani/octovy/pull/89.diff", + "patch_url": "https://github.com/m-mizutani/octovy/pull/89.patch", + "issue_url": "https://api.github.com/repos/m-mizutani/octovy/issues/89", + "number": 89, + "state": "open", + "locked": false, + "title": "v0.2.0", + "user": { + "login": "m-mizutani", + "id": 605953, + "node_id": "MDQ6VXNlcjYwNTk1Mw==", + "avatar_url": "https://avatars.githubusercontent.com/u/605953?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/m-mizutani", + "html_url": "https://github.com/m-mizutani", + "followers_url": "https://api.github.com/users/m-mizutani/followers", + "following_url": "https://api.github.com/users/m-mizutani/following{/other_user}", + "gists_url": "https://api.github.com/users/m-mizutani/gists{/gist_id}", + "starred_url": "https://api.github.com/users/m-mizutani/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/m-mizutani/subscriptions", + "organizations_url": "https://api.github.com/users/m-mizutani/orgs", + "repos_url": "https://api.github.com/users/m-mizutani/repos", + "events_url": "https://api.github.com/users/m-mizutani/events{/privacy}", + "received_events_url": "https://api.github.com/users/m-mizutani/received_events", + "type": "User", + "site_admin": false + }, + "body": "# Overview\r\n\r\n- Octovy's v0.1.x has become overly feature-rich, making continued maintenance difficult.\r\n - There were many implementations of UI that the developers were unfamiliar with.\r\n - In order to accommodate multiple use cases, the DB schema has become excessively complex.\r\n- In response to this, we will significantly renew the features in v0.2.0. This will involve a large amount of breaking changes.", + "created_at": "2023-08-14T01:55:48Z", + "updated_at": "2023-09-17T06:37:51Z", + "closed_at": null, + "merged_at": null, + "merge_commit_sha": "366e61bce88a7532ec2bfa9964a8399f36469e56", + "assignee": null, + "assignees": [], + "requested_reviewers": [], + "requested_teams": [], + "labels": [], + "milestone": null, + "draft": true, + "commits_url": "https://api.github.com/repos/m-mizutani/octovy/pulls/89/commits", + "review_comments_url": "https://api.github.com/repos/m-mizutani/octovy/pulls/89/comments", + "review_comment_url": "https://api.github.com/repos/m-mizutani/octovy/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/m-mizutani/octovy/issues/89/comments", + "statuses_url": "https://api.github.com/repos/m-mizutani/octovy/statuses/69454c171c2f0f2dbc9ccb0c9ef9b72fd769f046", + "head": { + "label": "m-mizutani:release/v0.2.0", + "ref": "release/v0.2.0", + "sha": "69454c171c2f0f2dbc9ccb0c9ef9b72fd769f046", + "user": { + "login": "m-mizutani", + "id": 605953, + "node_id": "MDQ6VXNlcjYwNTk1Mw==", + "avatar_url": "https://avatars.githubusercontent.com/u/605953?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/m-mizutani", + "html_url": "https://github.com/m-mizutani", + "followers_url": "https://api.github.com/users/m-mizutani/followers", + "following_url": "https://api.github.com/users/m-mizutani/following{/other_user}", + "gists_url": "https://api.github.com/users/m-mizutani/gists{/gist_id}", + "starred_url": "https://api.github.com/users/m-mizutani/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/m-mizutani/subscriptions", + "organizations_url": "https://api.github.com/users/m-mizutani/orgs", + "repos_url": "https://api.github.com/users/m-mizutani/repos", + "events_url": "https://api.github.com/users/m-mizutani/events{/privacy}", + "received_events_url": "https://api.github.com/users/m-mizutani/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 359010704, + "node_id": "MDEwOlJlcG9zaXRvcnkzNTkwMTA3MDQ=", + "name": "octovy", + "full_name": "m-mizutani/octovy", + "private": false, + "owner": { + "login": "m-mizutani", + "id": 605953, + "node_id": "MDQ6VXNlcjYwNTk1Mw==", + "avatar_url": "https://avatars.githubusercontent.com/u/605953?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/m-mizutani", + "html_url": "https://github.com/m-mizutani", + "followers_url": "https://api.github.com/users/m-mizutani/followers", + "following_url": "https://api.github.com/users/m-mizutani/following{/other_user}", + "gists_url": "https://api.github.com/users/m-mizutani/gists{/gist_id}", + "starred_url": "https://api.github.com/users/m-mizutani/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/m-mizutani/subscriptions", + "organizations_url": "https://api.github.com/users/m-mizutani/orgs", + "repos_url": "https://api.github.com/users/m-mizutani/repos", + "events_url": "https://api.github.com/users/m-mizutani/events{/privacy}", + "received_events_url": "https://api.github.com/users/m-mizutani/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/m-mizutani/octovy", + "description": "Trivy based vulnerability management service", + "fork": false, + "url": "https://api.github.com/repos/m-mizutani/octovy", + "forks_url": "https://api.github.com/repos/m-mizutani/octovy/forks", + "keys_url": "https://api.github.com/repos/m-mizutani/octovy/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/m-mizutani/octovy/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/m-mizutani/octovy/teams", + "hooks_url": "https://api.github.com/repos/m-mizutani/octovy/hooks", + "issue_events_url": "https://api.github.com/repos/m-mizutani/octovy/issues/events{/number}", + "events_url": "https://api.github.com/repos/m-mizutani/octovy/events", + "assignees_url": "https://api.github.com/repos/m-mizutani/octovy/assignees{/user}", + "branches_url": "https://api.github.com/repos/m-mizutani/octovy/branches{/branch}", + "tags_url": "https://api.github.com/repos/m-mizutani/octovy/tags", + "blobs_url": "https://api.github.com/repos/m-mizutani/octovy/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/m-mizutani/octovy/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/m-mizutani/octovy/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/m-mizutani/octovy/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/m-mizutani/octovy/statuses/{sha}", + "languages_url": "https://api.github.com/repos/m-mizutani/octovy/languages", + "stargazers_url": "https://api.github.com/repos/m-mizutani/octovy/stargazers", + "contributors_url": "https://api.github.com/repos/m-mizutani/octovy/contributors", + "subscribers_url": "https://api.github.com/repos/m-mizutani/octovy/subscribers", + "subscription_url": "https://api.github.com/repos/m-mizutani/octovy/subscription", + "commits_url": "https://api.github.com/repos/m-mizutani/octovy/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/m-mizutani/octovy/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/m-mizutani/octovy/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/m-mizutani/octovy/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/m-mizutani/octovy/contents/{+path}", + "compare_url": "https://api.github.com/repos/m-mizutani/octovy/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/m-mizutani/octovy/merges", + "archive_url": "https://api.github.com/repos/m-mizutani/octovy/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/m-mizutani/octovy/downloads", + "issues_url": "https://api.github.com/repos/m-mizutani/octovy/issues{/number}", + "pulls_url": "https://api.github.com/repos/m-mizutani/octovy/pulls{/number}", + "milestones_url": "https://api.github.com/repos/m-mizutani/octovy/milestones{/number}", + "notifications_url": "https://api.github.com/repos/m-mizutani/octovy/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/m-mizutani/octovy/labels{/name}", + "releases_url": "https://api.github.com/repos/m-mizutani/octovy/releases{/id}", + "deployments_url": "https://api.github.com/repos/m-mizutani/octovy/deployments", + "created_at": "2021-04-18T00:25:50Z", + "updated_at": "2023-08-10T05:59:13Z", + "pushed_at": "2023-09-17T06:37:52Z", + "git_url": "git://github.com/m-mizutani/octovy.git", + "ssh_url": "git@github.com:m-mizutani/octovy.git", + "clone_url": "https://github.com/m-mizutani/octovy.git", + "svn_url": "https://github.com/m-mizutani/octovy", + "homepage": "", + "size": 2230, + "stargazers_count": 51, + "watchers_count": 51, + "language": "Go", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 4, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 57, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + "github-app", + "golang", + "security", + "typescript", + "vulnerability-scanners" + ], + "visibility": "public", + "forks": 4, + "open_issues": 57, + "watchers": 51, + "default_branch": "main", + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "allow_auto_merge": false, + "delete_branch_on_merge": true, + "allow_update_branch": false, + "use_squash_pr_title_as_default": false, + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE" + } + }, + "base": { + "label": "m-mizutani:main", + "ref": "main", + "sha": "08fb7816c6d0a485239ca5f342342186f972a6e7", + "user": { + "login": "m-mizutani", + "id": 605953, + "node_id": "MDQ6VXNlcjYwNTk1Mw==", + "avatar_url": "https://avatars.githubusercontent.com/u/605953?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/m-mizutani", + "html_url": "https://github.com/m-mizutani", + "followers_url": "https://api.github.com/users/m-mizutani/followers", + "following_url": "https://api.github.com/users/m-mizutani/following{/other_user}", + "gists_url": "https://api.github.com/users/m-mizutani/gists{/gist_id}", + "starred_url": "https://api.github.com/users/m-mizutani/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/m-mizutani/subscriptions", + "organizations_url": "https://api.github.com/users/m-mizutani/orgs", + "repos_url": "https://api.github.com/users/m-mizutani/repos", + "events_url": "https://api.github.com/users/m-mizutani/events{/privacy}", + "received_events_url": "https://api.github.com/users/m-mizutani/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 359010704, + "node_id": "MDEwOlJlcG9zaXRvcnkzNTkwMTA3MDQ=", + "name": "octovy", + "full_name": "m-mizutani/octovy", + "private": false, + "owner": { + "login": "m-mizutani", + "id": 605953, + "node_id": "MDQ6VXNlcjYwNTk1Mw==", + "avatar_url": "https://avatars.githubusercontent.com/u/605953?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/m-mizutani", + "html_url": "https://github.com/m-mizutani", + "followers_url": "https://api.github.com/users/m-mizutani/followers", + "following_url": "https://api.github.com/users/m-mizutani/following{/other_user}", + "gists_url": "https://api.github.com/users/m-mizutani/gists{/gist_id}", + "starred_url": "https://api.github.com/users/m-mizutani/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/m-mizutani/subscriptions", + "organizations_url": "https://api.github.com/users/m-mizutani/orgs", + "repos_url": "https://api.github.com/users/m-mizutani/repos", + "events_url": "https://api.github.com/users/m-mizutani/events{/privacy}", + "received_events_url": "https://api.github.com/users/m-mizutani/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/m-mizutani/octovy", + "description": "Trivy based vulnerability management service", + "fork": false, + "url": "https://api.github.com/repos/m-mizutani/octovy", + "forks_url": "https://api.github.com/repos/m-mizutani/octovy/forks", + "keys_url": "https://api.github.com/repos/m-mizutani/octovy/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/m-mizutani/octovy/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/m-mizutani/octovy/teams", + "hooks_url": "https://api.github.com/repos/m-mizutani/octovy/hooks", + "issue_events_url": "https://api.github.com/repos/m-mizutani/octovy/issues/events{/number}", + "events_url": "https://api.github.com/repos/m-mizutani/octovy/events", + "assignees_url": "https://api.github.com/repos/m-mizutani/octovy/assignees{/user}", + "branches_url": "https://api.github.com/repos/m-mizutani/octovy/branches{/branch}", + "tags_url": "https://api.github.com/repos/m-mizutani/octovy/tags", + "blobs_url": "https://api.github.com/repos/m-mizutani/octovy/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/m-mizutani/octovy/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/m-mizutani/octovy/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/m-mizutani/octovy/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/m-mizutani/octovy/statuses/{sha}", + "languages_url": "https://api.github.com/repos/m-mizutani/octovy/languages", + "stargazers_url": "https://api.github.com/repos/m-mizutani/octovy/stargazers", + "contributors_url": "https://api.github.com/repos/m-mizutani/octovy/contributors", + "subscribers_url": "https://api.github.com/repos/m-mizutani/octovy/subscribers", + "subscription_url": "https://api.github.com/repos/m-mizutani/octovy/subscription", + "commits_url": "https://api.github.com/repos/m-mizutani/octovy/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/m-mizutani/octovy/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/m-mizutani/octovy/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/m-mizutani/octovy/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/m-mizutani/octovy/contents/{+path}", + "compare_url": "https://api.github.com/repos/m-mizutani/octovy/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/m-mizutani/octovy/merges", + "archive_url": "https://api.github.com/repos/m-mizutani/octovy/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/m-mizutani/octovy/downloads", + "issues_url": "https://api.github.com/repos/m-mizutani/octovy/issues{/number}", + "pulls_url": "https://api.github.com/repos/m-mizutani/octovy/pulls{/number}", + "milestones_url": "https://api.github.com/repos/m-mizutani/octovy/milestones{/number}", + "notifications_url": "https://api.github.com/repos/m-mizutani/octovy/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/m-mizutani/octovy/labels{/name}", + "releases_url": "https://api.github.com/repos/m-mizutani/octovy/releases{/id}", + "deployments_url": "https://api.github.com/repos/m-mizutani/octovy/deployments", + "created_at": "2021-04-18T00:25:50Z", + "updated_at": "2023-08-10T05:59:13Z", + "pushed_at": "2023-09-17T06:37:52Z", + "git_url": "git://github.com/m-mizutani/octovy.git", + "ssh_url": "git@github.com:m-mizutani/octovy.git", + "clone_url": "https://github.com/m-mizutani/octovy.git", + "svn_url": "https://github.com/m-mizutani/octovy", + "homepage": "", + "size": 2230, + "stargazers_count": 51, + "watchers_count": 51, + "language": "Go", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 4, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 57, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + "github-app", + "golang", + "security", + "typescript", + "vulnerability-scanners" + ], + "visibility": "public", + "forks": 4, + "open_issues": 57, + "watchers": 51, + "default_branch": "main", + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "allow_auto_merge": false, + "delete_branch_on_merge": true, + "allow_update_branch": false, + "use_squash_pr_title_as_default": false, + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/m-mizutani/octovy/pulls/89" + }, + "html": { + "href": "https://github.com/m-mizutani/octovy/pull/89" + }, + "issue": { + "href": "https://api.github.com/repos/m-mizutani/octovy/issues/89" + }, + "comments": { + "href": "https://api.github.com/repos/m-mizutani/octovy/issues/89/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/m-mizutani/octovy/pulls/89/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/m-mizutani/octovy/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/m-mizutani/octovy/pulls/89/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/m-mizutani/octovy/statuses/69454c171c2f0f2dbc9ccb0c9ef9b72fd769f046" + } + }, + "author_association": "OWNER", + "auto_merge": null, + "active_lock_reason": null, + "merged": false, + "mergeable": null, + "rebaseable": null, + "mergeable_state": "unknown", + "merged_by": null, + "comments": 0, + "review_comments": 3, + "maintainer_can_modify": false, + "commits": 22, + "additions": 3399, + "deletions": 72648, + "changed_files": 273 + }, + "before": "bca5ddd2023d5c906a0420492deb2ede8d99eb79", + "after": "69454c171c2f0f2dbc9ccb0c9ef9b72fd769f046", + "repository": { + "id": 359010704, + "node_id": "MDEwOlJlcG9zaXRvcnkzNTkwMTA3MDQ=", + "name": "octovy", + "full_name": "m-mizutani/octovy", + "private": false, + "owner": { + "login": "m-mizutani", + "id": 605953, + "node_id": "MDQ6VXNlcjYwNTk1Mw==", + "avatar_url": "https://avatars.githubusercontent.com/u/605953?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/m-mizutani", + "html_url": "https://github.com/m-mizutani", + "followers_url": "https://api.github.com/users/m-mizutani/followers", + "following_url": "https://api.github.com/users/m-mizutani/following{/other_user}", + "gists_url": "https://api.github.com/users/m-mizutani/gists{/gist_id}", + "starred_url": "https://api.github.com/users/m-mizutani/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/m-mizutani/subscriptions", + "organizations_url": "https://api.github.com/users/m-mizutani/orgs", + "repos_url": "https://api.github.com/users/m-mizutani/repos", + "events_url": "https://api.github.com/users/m-mizutani/events{/privacy}", + "received_events_url": "https://api.github.com/users/m-mizutani/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/m-mizutani/octovy", + "description": "Trivy based vulnerability management service", + "fork": false, + "url": "https://api.github.com/repos/m-mizutani/octovy", + "forks_url": "https://api.github.com/repos/m-mizutani/octovy/forks", + "keys_url": "https://api.github.com/repos/m-mizutani/octovy/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/m-mizutani/octovy/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/m-mizutani/octovy/teams", + "hooks_url": "https://api.github.com/repos/m-mizutani/octovy/hooks", + "issue_events_url": "https://api.github.com/repos/m-mizutani/octovy/issues/events{/number}", + "events_url": "https://api.github.com/repos/m-mizutani/octovy/events", + "assignees_url": "https://api.github.com/repos/m-mizutani/octovy/assignees{/user}", + "branches_url": "https://api.github.com/repos/m-mizutani/octovy/branches{/branch}", + "tags_url": "https://api.github.com/repos/m-mizutani/octovy/tags", + "blobs_url": "https://api.github.com/repos/m-mizutani/octovy/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/m-mizutani/octovy/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/m-mizutani/octovy/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/m-mizutani/octovy/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/m-mizutani/octovy/statuses/{sha}", + "languages_url": "https://api.github.com/repos/m-mizutani/octovy/languages", + "stargazers_url": "https://api.github.com/repos/m-mizutani/octovy/stargazers", + "contributors_url": "https://api.github.com/repos/m-mizutani/octovy/contributors", + "subscribers_url": "https://api.github.com/repos/m-mizutani/octovy/subscribers", + "subscription_url": "https://api.github.com/repos/m-mizutani/octovy/subscription", + "commits_url": "https://api.github.com/repos/m-mizutani/octovy/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/m-mizutani/octovy/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/m-mizutani/octovy/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/m-mizutani/octovy/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/m-mizutani/octovy/contents/{+path}", + "compare_url": "https://api.github.com/repos/m-mizutani/octovy/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/m-mizutani/octovy/merges", + "archive_url": "https://api.github.com/repos/m-mizutani/octovy/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/m-mizutani/octovy/downloads", + "issues_url": "https://api.github.com/repos/m-mizutani/octovy/issues{/number}", + "pulls_url": "https://api.github.com/repos/m-mizutani/octovy/pulls{/number}", + "milestones_url": "https://api.github.com/repos/m-mizutani/octovy/milestones{/number}", + "notifications_url": "https://api.github.com/repos/m-mizutani/octovy/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/m-mizutani/octovy/labels{/name}", + "releases_url": "https://api.github.com/repos/m-mizutani/octovy/releases{/id}", + "deployments_url": "https://api.github.com/repos/m-mizutani/octovy/deployments", + "created_at": "2021-04-18T00:25:50Z", + "updated_at": "2023-08-10T05:59:13Z", + "pushed_at": "2023-09-17T06:37:52Z", + "git_url": "git://github.com/m-mizutani/octovy.git", + "ssh_url": "git@github.com:m-mizutani/octovy.git", + "clone_url": "https://github.com/m-mizutani/octovy.git", + "svn_url": "https://github.com/m-mizutani/octovy", + "homepage": "", + "size": 2230, + "stargazers_count": 51, + "watchers_count": 51, + "language": "Go", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 4, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 57, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + "github-app", + "golang", + "security", + "typescript", + "vulnerability-scanners" + ], + "visibility": "public", + "forks": 4, + "open_issues": 57, + "watchers": 51, + "default_branch": "main" + }, + "sender": { + "login": "m-mizutani", + "id": 605953, + "node_id": "MDQ6VXNlcjYwNTk1Mw==", + "avatar_url": "https://avatars.githubusercontent.com/u/605953?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/m-mizutani", + "html_url": "https://github.com/m-mizutani", + "followers_url": "https://api.github.com/users/m-mizutani/followers", + "following_url": "https://api.github.com/users/m-mizutani/following{/other_user}", + "gists_url": "https://api.github.com/users/m-mizutani/gists{/gist_id}", + "starred_url": "https://api.github.com/users/m-mizutani/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/m-mizutani/subscriptions", + "organizations_url": "https://api.github.com/users/m-mizutani/orgs", + "repos_url": "https://api.github.com/users/m-mizutani/repos", + "events_url": "https://api.github.com/users/m-mizutani/events{/privacy}", + "received_events_url": "https://api.github.com/users/m-mizutani/received_events", + "type": "User", + "site_admin": false + }, + "installation": { + "id": 41633205, + "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNDE2MzMyMDU=" + } +} diff --git a/pkg/controller/server/testdata/github/pull_request.synchronize.json b/pkg/controller/server/testdata/github/pull_request.synchronize.json index fe7fd3fa..0eb55aff 100644 --- a/pkg/controller/server/testdata/github/pull_request.synchronize.json +++ b/pkg/controller/server/testdata/github/pull_request.synchronize.json @@ -45,7 +45,7 @@ "requested_teams": [], "labels": [], "milestone": null, - "draft": true, + "draft": false, "commits_url": "https://api.github.com/repos/m-mizutani/octovy/pulls/89/commits", "review_comments_url": "https://api.github.com/repos/m-mizutani/octovy/pulls/89/comments", "review_comment_url": "https://api.github.com/repos/m-mizutani/octovy/pulls/comments{/number}", diff --git a/pkg/domain/interfaces/infra.go b/pkg/domain/interfaces/infra.go new file mode 100644 index 00000000..a96ef9ac --- /dev/null +++ b/pkg/domain/interfaces/infra.go @@ -0,0 +1,47 @@ +package interfaces + +import ( + "context" + "io" + "net/url" + + "cloud.google.com/go/bigquery" + + "github.com/google/go-github/v53/github" + "github.com/m-mizutani/octovy/pkg/domain/model" + "github.com/m-mizutani/octovy/pkg/domain/types" + "github.com/m-mizutani/opac" +) + +type BigQuery interface { + Insert(ctx context.Context, tableID types.BQTableID, schema bigquery.Schema, data any) error + + GetMetadata(ctx context.Context, table types.BQTableID) (*bigquery.TableMetadata, error) + UpdateTable(ctx context.Context, table types.BQTableID, md bigquery.TableMetadataToUpdate, eTag string) error + CreateTable(ctx context.Context, table types.BQTableID, md *bigquery.TableMetadata) error +} + +type Storage interface { + Put(ctx context.Context, key string, r io.ReadCloser) error + Get(ctx context.Context, key string) (io.ReadCloser, error) +} + +type GitHub interface { + GetArchiveURL(ctx context.Context, input *GetArchiveURLInput) (*url.URL, error) + CreateIssueComment(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, prID int, body string) error + ListIssueComments(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, prID int) ([]*model.GitHubIssueComment, error) + MinimizeComment(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, subjectID string) error + CreateCheckRun(ctx context.Context, id types.GitHubAppInstallID, repo *model.GitHubRepo, commit string) (int64, error) + UpdateCheckRun(ctx context.Context, id types.GitHubAppInstallID, repo *model.GitHubRepo, checkID int64, opt *github.UpdateCheckRunOptions) error +} + +type GetArchiveURLInput struct { + Owner string + Repo string + CommitID string + InstallID types.GitHubAppInstallID +} + +type Policy interface { + Query(ctx context.Context, query string, input, output any, options ...opac.QueryOption) error +} diff --git a/pkg/domain/interfaces/mock.go b/pkg/domain/interfaces/mock.go new file mode 100644 index 00000000..c90d82e6 --- /dev/null +++ b/pkg/domain/interfaces/mock.go @@ -0,0 +1,90 @@ +package interfaces + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/url" + + "github.com/google/go-github/v53/github" + "github.com/m-mizutani/goerr" + "github.com/m-mizutani/octovy/pkg/domain/model" + "github.com/m-mizutani/octovy/pkg/domain/types" +) + +type StorageMock struct { + Data map[string][]byte +} + +var _ Storage = (*StorageMock)(nil) + +func NewStorageMock() *StorageMock { + return &StorageMock{ + Data: make(map[string][]byte), + } +} + +// Get implements Storage. +func (s *StorageMock) Get(ctx context.Context, key string) (io.ReadCloser, error) { + if data, ok := s.Data[key]; ok { + return io.NopCloser(bytes.NewReader(data)), nil + } + return nil, nil +} + +// Put implements Storage. +func (s *StorageMock) Put(ctx context.Context, key string, r io.ReadCloser) error { + data, err := io.ReadAll(r) + if err != nil { + return err + } + s.Data[key] = data + return nil +} + +func (s *StorageMock) Unmarshal(key string, v interface{}) error { + data, ok := s.Data[key] + if !ok { + return io.EOF + } + + if err := json.Unmarshal(data, v); err != nil { + return goerr.Wrap(err, "Failed to unmarshal data") + } + + return nil +} + +type GitHubMock struct { + MockGetArchiveURL func(ctx context.Context, input *GetArchiveURLInput) (*url.URL, error) + MockCreateIssueComment func(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, prID int, body string) error + MockListIssueComments func(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, prID int) ([]*model.GitHubIssueComment, error) + MockMinimizeComment func(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, subjectID string) error + MockCreateCheckRun func(ctx context.Context, id types.GitHubAppInstallID, repo *model.GitHubRepo, commit string) (int64, error) + MockUpdateCheckRun func(ctx context.Context, id types.GitHubAppInstallID, repo *model.GitHubRepo, checkID int64, opt *github.UpdateCheckRunOptions) error +} + +func (x *GitHubMock) GetArchiveURL(ctx context.Context, input *GetArchiveURLInput) (*url.URL, error) { + return x.MockGetArchiveURL(ctx, input) +} + +func (x *GitHubMock) CreateIssueComment(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, prID int, body string) error { + return x.MockCreateIssueComment(ctx, repo, id, prID, body) +} + +func (x *GitHubMock) ListIssueComments(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, prID int) ([]*model.GitHubIssueComment, error) { + return x.MockListIssueComments(ctx, repo, id, prID) +} + +func (x *GitHubMock) MinimizeComment(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, subjectID string) error { + return x.MockMinimizeComment(ctx, repo, id, subjectID) +} + +func (x *GitHubMock) CreateCheckRun(ctx context.Context, id types.GitHubAppInstallID, repo *model.GitHubRepo, commit string) (int64, error) { + return x.MockCreateCheckRun(ctx, id, repo, commit) +} + +func (x *GitHubMock) UpdateCheckRun(ctx context.Context, id types.GitHubAppInstallID, repo *model.GitHubRepo, checkID int64, opt *github.UpdateCheckRunOptions) error { + return x.MockUpdateCheckRun(ctx, id, repo, checkID, opt) +} diff --git a/pkg/domain/interfaces/test.go b/pkg/domain/interfaces/test.go new file mode 100644 index 00000000..53fde6b8 --- /dev/null +++ b/pkg/domain/interfaces/test.go @@ -0,0 +1,87 @@ +package interfaces + +/* +func FirestoreClientTest(t *testing.T, client Firestore) { + type testData struct { + Value string `firestore:"value"` + } + + type testCase struct { + get []types.FireStoreRef + put []types.FireStoreRef + input testData + output *testData + } + + prefix := "test-" + uuid.NewString() + "-" + p := func(id string) types.FSCollectionID { + return types.FSCollectionID(prefix + id) + } + + testCases := map[string]testCase{ + "simple put and get": { + put: []types.FireStoreRef{{CollectionID: p("t1"), DocumentID: "doc1"}}, + get: []types.FireStoreRef{{CollectionID: p("t1"), DocumentID: "doc1"}}, + input: testData{ + Value: "hello", + }, + output: &testData{ + Value: "hello", + }, + }, + "nested put and get": { + put: []types.FireStoreRef{{CollectionID: p("t2"), DocumentID: "doc1"}, {CollectionID: p("col2"), DocumentID: "doc2"}}, + get: []types.FireStoreRef{{CollectionID: p("t2"), DocumentID: "doc1"}, {CollectionID: p("col2"), DocumentID: "doc2"}}, + input: testData{ + Value: "hello", + }, + output: &testData{ + Value: "hello", + }, + }, + "nested put and get with different collection": { + put: []types.FireStoreRef{{CollectionID: p("t3"), DocumentID: "doc1"}, {CollectionID: p("col2"), DocumentID: "doc3"}}, + get: []types.FireStoreRef{{CollectionID: p("t3"), DocumentID: "doc1"}, {CollectionID: p("col3"), DocumentID: "doc4"}}, + input: testData{ + Value: "hello", + }, + output: nil, + }, + "nested put and get with different document": { + put: []types.FireStoreRef{{CollectionID: p("t4"), DocumentID: "doc1"}, {CollectionID: p("col2"), DocumentID: "doc5"}}, + get: []types.FireStoreRef{{CollectionID: p("t4"), DocumentID: "doc2"}, {CollectionID: p("col2"), DocumentID: "doc5"}}, + input: testData{ + Value: "hello", + }, + output: nil, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + + if tc.put != nil { + if err := client.Put(ctx, tc.input, tc.put...); err != nil { + t.Fatal(err) + } + } + + if tc.get != nil { + var output *testData + if err := client.Get(ctx, &output, tc.get...); err != nil { + t.Fatal(err) + } + + if tc.output == nil && output != nil { + t.Errorf("Unexpected output: %v", output) + } else if tc.output != nil && output == nil { + t.Errorf("No output") + } else if tc.output != nil && output != nil { + gt.Equal(t, tc.output.Value, output.Value) + } + } + }) + } +} +*/ diff --git a/pkg/domain/interfaces/usecase.go b/pkg/domain/interfaces/usecase.go new file mode 100644 index 00000000..ae6e7261 --- /dev/null +++ b/pkg/domain/interfaces/usecase.go @@ -0,0 +1,13 @@ +package interfaces + +import ( + "context" + + "github.com/m-mizutani/octovy/pkg/domain/model" + "github.com/m-mizutani/octovy/pkg/domain/model/trivy" +) + +type UseCase interface { + InsertScanResult(ctx context.Context, meta model.GitHubMetadata, report trivy.Report) error + ScanGitHubRepo(ctx context.Context, input *model.ScanGitHubRepoInput) error +} diff --git a/pkg/domain/logic/diff.go b/pkg/domain/logic/diff.go new file mode 100644 index 00000000..98f6e8b3 --- /dev/null +++ b/pkg/domain/logic/diff.go @@ -0,0 +1,60 @@ +package logic + +import "github.com/m-mizutani/octovy/pkg/domain/model/trivy" + +func DiffResults(oldReport, newReport *trivy.Report) (fixed, added trivy.Results) { + resultMap := map[string]trivy.Result{} + for _, result := range oldReport.Results { + resultMap[result.Target] = result + } + + for _, newResult := range newReport.Results { + oldResult, ok := resultMap[newResult.Target] + if !ok { + added = append(added, newResult) + continue + } + + fixedVuln, addedVuln := DiffVulnerabilities(oldResult.Vulnerabilities, newResult.Vulnerabilities) + if len(fixedVuln) > 0 { + fixedResult := oldResult + fixedResult.Vulnerabilities = fixedVuln + fixed = append(fixed, fixedResult) + } + + if len(addedVuln) > 0 { + addedResult := newResult + addedResult.Vulnerabilities = addedVuln + added = append(added, addedResult) + } + + delete(resultMap, newResult.Target) + } + + for _, result := range resultMap { + fixed = append(fixed, result) + } + + return +} + +func DiffVulnerabilities(oldVulns, newVulns []trivy.DetectedVulnerability) (fixed, added []trivy.DetectedVulnerability) { + oldVulnMap := map[string]trivy.DetectedVulnerability{} + for _, vuln := range oldVulns { + + oldVulnMap[vuln.ID()] = vuln + } + + for _, newVuln := range newVulns { + if _, ok := oldVulnMap[newVuln.ID()]; !ok { + added = append(added, newVuln) + } + delete(oldVulnMap, newVuln.ID()) + } + + for _, vuln := range oldVulnMap { + fixed = append(fixed, vuln) + } + + return +} diff --git a/pkg/domain/logic/diff_test.go b/pkg/domain/logic/diff_test.go new file mode 100644 index 00000000..748896af --- /dev/null +++ b/pkg/domain/logic/diff_test.go @@ -0,0 +1,292 @@ +package logic_test + +import ( + "testing" + + "github.com/m-mizutani/gt" + "github.com/m-mizutani/octovy/pkg/domain/logic" + "github.com/m-mizutani/octovy/pkg/domain/model/trivy" +) + +func TestDiffResults(t *testing.T) { + type testCase struct { + oldReport, newReport trivy.Report + fixed, added trivy.Results + } + + test := func(c testCase) func(t *testing.T) { + return func(t *testing.T) { + fixed, added := logic.DiffResults(&c.oldReport, &c.newReport) + gt.Equal(t, fixed, c.fixed) + gt.Equal(t, added, c.added) + } + } + + t.Run("No diff", test(testCase{ + oldReport: trivy.Report{ + Results: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + }, + }, + }, + }, + newReport: trivy.Report{ + Results: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + }, + }, + }, + }, + fixed: nil, + added: nil, + })) + + t.Run("Add new vulnerability", test(testCase{ + oldReport: trivy.Report{ + Results: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + }, + }, + }, + }, + newReport: trivy.Report{ + Results: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + {VulnerabilityID: "CVE-0000-0002", PkgName: "pkg2"}, + }, + }, + }, + }, + fixed: nil, + added: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0002", PkgName: "pkg2"}, + }, + }, + }, + })) + + t.Run("Fix vulnerability", test(testCase{ + oldReport: trivy.Report{ + Results: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + {VulnerabilityID: "CVE-0000-0002", PkgName: "pkg2"}, + }, + }, + }, + }, + newReport: trivy.Report{ + Results: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + }, + }, + }, + }, + fixed: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0002", PkgName: "pkg2"}, + }, + }, + }, + added: nil, + })) + + t.Run("Add and fix vulnerability", test(testCase{ + oldReport: trivy.Report{ + Results: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + {VulnerabilityID: "CVE-0000-0002", PkgName: "pkg2"}, + }, + }, + }, + }, + newReport: trivy.Report{ + Results: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + + {VulnerabilityID: "CVE-0000-0003", PkgName: "pkg3"}, + + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + }, + }, + }, + }, + fixed: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0002", PkgName: "pkg2"}, + }, + }, + }, + added: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0003", PkgName: "pkg3"}, + }, + }, + }, + })) + + t.Run("No diff with multiple results", test(testCase{ + oldReport: trivy.Report{ + Results: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + }, + }, + { + Target: "target2", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0002", PkgName: "pkg2"}, + }, + }, + }, + }, + newReport: trivy.Report{ + Results: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + }, + }, + { + Target: "target2", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0002", PkgName: "pkg2"}, + }, + }, + }, + }, + fixed: nil, + added: nil, + })) + + t.Run("Add new vulnerability with multiple results", test(testCase{ + oldReport: trivy.Report{ + Results: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + }, + }, + { + Target: "target2", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0002", PkgName: "pkg2"}, + }, + }, + }, + }, + newReport: trivy.Report{ + Results: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + {VulnerabilityID: "CVE-0000-0002", PkgName: "pkg2"}, + }, + }, + { + Target: "target2", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + {VulnerabilityID: "CVE-0000-0002", PkgName: "pkg2"}, + }, + }, + }, + }, + fixed: nil, + added: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0002", PkgName: "pkg2"}, + }, + }, + { + Target: "target2", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + }, + }, + }, + })) + + t.Run("Fix vulnerability with multiple results", test(testCase{ + oldReport: trivy.Report{ + Results: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + {VulnerabilityID: "CVE-0000-0002", PkgName: "pkg2"}, + }, + }, + { + Target: "target2", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0003", PkgName: "pkg3"}, + }, + }, + }, + }, + newReport: trivy.Report{ + Results: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1"}, + }, + }, + { + Target: "target2", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0003", PkgName: "pkg3"}, + }, + }, + }, + }, + fixed: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0002", PkgName: "pkg2"}, + }, + }, + }, + added: nil, + })) +} diff --git a/pkg/domain/model/bigquery.go b/pkg/domain/model/bigquery.go new file mode 100644 index 00000000..8b537907 --- /dev/null +++ b/pkg/domain/model/bigquery.go @@ -0,0 +1 @@ +package model diff --git a/pkg/domain/model/context.go b/pkg/domain/model/context.go deleted file mode 100644 index 7a475a9f..00000000 --- a/pkg/domain/model/context.go +++ /dev/null @@ -1,49 +0,0 @@ -package model - -import ( - "context" - "log/slog" - - "github.com/m-mizutani/octovy/pkg/utils" -) - -type Context struct { - logger *slog.Logger - context.Context -} - -func (x *Context) Logger() *slog.Logger { return x.logger } -func (x *Context) New(options ...Option) *Context { - newCtx := *x - for _, opt := range options { - opt(&newCtx) - } - return &newCtx -} - -func NewContext(options ...Option) *Context { - ctx := &Context{ - logger: utils.Logger(), - Context: context.Background(), - } - - for _, opt := range options { - opt(ctx) - } - - return ctx -} - -type Option func(*Context) - -func WithLogger(logger *slog.Logger) Option { - return func(ctx *Context) { - ctx.logger = logger - } -} - -func WithBase(base context.Context) Option { - return func(ctx *Context) { - ctx.Context = base - } -} diff --git a/pkg/domain/model/github.go b/pkg/domain/model/github.go index 8d388097..af285858 100644 --- a/pkg/domain/model/github.go +++ b/pkg/domain/model/github.go @@ -1,25 +1,27 @@ package model import ( + "regexp" + "github.com/m-mizutani/goerr" "github.com/m-mizutani/octovy/pkg/domain/types" ) type GitHubRepo struct { - RepoID int64 - Owner string - RepoName string + RepoID int64 `json:"repo_id" bigquery:"repo_id"` + Owner string `json:"owner" bigquery:"owner"` + RepoName string `json:"repo_name" bigquery:"repo_name"` } func (x *GitHubRepo) Validate() error { if x.RepoID == 0 { - return goerr.Wrap(types.ErrInvalidOption, "repo ID is empty") + return goerr.Wrap(types.ErrValidationFailed, "repo ID is empty") } if x.Owner == "" { - return goerr.Wrap(types.ErrInvalidOption, "owner is empty") + return goerr.Wrap(types.ErrValidationFailed, "owner is empty") } if x.RepoName == "" { - return goerr.Wrap(types.ErrInvalidOption, "repo name is empty") + return goerr.Wrap(types.ErrValidationFailed, "repo name is empty") } return nil @@ -27,16 +29,51 @@ func (x *GitHubRepo) Validate() error { type GitHubCommit struct { GitHubRepo - CommitID string + Committer GitHubUser `json:"committer" bigquery:"committer"` + CommitID string `json:"commit_id" bigquery:"commit_id"` + Branch string `json:"branch" bigquery:"branch"` + Ref string `json:"ref" bigquery:"ref"` +} + +type GitHubMetadata struct { + GitHubCommit + PullRequest *GitHubPullRequest `json:"pull_request"` + DefaultBranch string `json:"default_branch"` +} + +type GitHubPullRequest struct { + ID int64 `json:"id"` + Number int `json:"number"` + BaseBranch string `json:"base_branch"` + BaseCommitID string `json:"base_commit_id"` + User GitHubUser `json:"user"` +} + +type GitHubUser struct { + ID int64 `json:"id"` + Login string `json:"login"` + Email string `json:"email"` } +var ( + ptnValidCommitID = regexp.MustCompile("^[0-9a-f]{40}$") +) + func (x *GitHubCommit) Validate() error { if err := x.GitHubRepo.Validate(); err != nil { return err } - if x.CommitID == "" { - return goerr.Wrap(types.ErrInvalidOption, "commit ID is empty") + + if !ptnValidCommitID.MatchString(x.CommitID) { + return goerr.Wrap(types.ErrValidationFailed, "invalid commit ID") } return nil } + +type GitHubIssueComment struct { + ID string + Login string + Body string + IsMinimized bool +} diff --git a/pkg/domain/model/github_test.go b/pkg/domain/model/github_test.go new file mode 100644 index 00000000..43fc6f21 --- /dev/null +++ b/pkg/domain/model/github_test.go @@ -0,0 +1 @@ +package model_test diff --git a/pkg/domain/model/result.go b/pkg/domain/model/result.go new file mode 100644 index 00000000..4f25bd5c --- /dev/null +++ b/pkg/domain/model/result.go @@ -0,0 +1,20 @@ +package model + +import ( + "time" + + "github.com/m-mizutani/octovy/pkg/domain/model/trivy" + "github.com/m-mizutani/octovy/pkg/domain/types" +) + +type Scan struct { + ID types.ScanID `bigquery:"id" firestore:"id" json:"id"` + Timestamp time.Time `bigquery:"timestamp" firestore:"timestamp" json:"timestamp"` + GitHub GitHubMetadata `bigquery:"github" firestore:"github" json:"github"` + Report trivy.Report `bigquery:"report" firestore:"report" json:"report"` +} + +type ScanRawRecord struct { + Scan + Timestamp int64 `bigquery:"timestamp" firestore:"timestamp" json:"timestamp"` +} diff --git a/pkg/domain/model/trivy/detected_license.go b/pkg/domain/model/trivy/detected_license.go new file mode 100644 index 00000000..71775edc --- /dev/null +++ b/pkg/domain/model/trivy/detected_license.go @@ -0,0 +1,29 @@ +package trivy + +type DetectedLicense struct { + // Severity is the consistent parameter indicating how severe the issue is + Severity string + + // Category holds the license category such as "forbidden" + Category LicenseCategory + + // PkgName holds a package name of the license. + // It will be empty if FilePath is filled. + PkgName string + + // PkgName holds a file path of the license. + // It will be empty if PkgName is filled. + FilePath string // for file license + + // Name holds a detected license name + Name string + + // Confidence is level of the match. The confidence level is between 0.0 and 1.0, with 1.0 indicating an + // exact match and 0.0 indicating a complete mismatch + Confidence float64 + + // Link is a SPDX link of the license + Link string +} + +type LicenseCategory string diff --git a/pkg/domain/model/trivy/image.go b/pkg/domain/model/trivy/image.go new file mode 100644 index 00000000..197ae5b7 --- /dev/null +++ b/pkg/domain/model/trivy/image.go @@ -0,0 +1,99 @@ +package trivy + +import "time" + +type ConfigFile struct { + Architecture string `json:"architecture"` + Author string `json:"author,omitempty"` + Container string `json:"container,omitempty"` + // Created Time `json:"created,omitempty"` + Created string `json:"created,omitempty"` + DockerVersion string `json:"docker_version,omitempty"` + History []History `json:"history,omitempty"` + OS string `json:"os"` + RootFS RootFS `json:"rootfs"` + Config Config `json:"config"` + + // BigQuery does not support a field name with a dot, so we need to skip this field. + // OSVersion string `json:"os.version,omitempty"` + // OSFeatures []string `json:"os.features,omitempty"` + + Variant string `json:"variant,omitempty"` +} + +type History struct { + Author string `json:"author,omitempty"` + Created string `json:"created,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + Comment string `json:"comment,omitempty"` + EmptyLayer bool `json:"empty_layer,omitempty"` +} + +type OS struct { + Family string + Name string + Eosl bool `json:"EOSL,omitempty"` + + // This field is used for enhanced security maintenance programs such as Ubuntu ESM, Debian Extended LTS. + Extended bool `json:"extended,omitempty"` +} + +type RootFS struct { + Type string `json:"type"` + DiffIDs []Hash `json:"diff_ids"` +} + +type Hash struct { + // Algorithm holds the algorithm used to compute the hash. + Algorithm string + + // Hex holds the hex portion of the content hash. + Hex string +} + +type Config struct { + AttachStderr bool `json:"AttachStderr,omitempty"` + AttachStdin bool `json:"AttachStdin,omitempty"` + AttachStdout bool `json:"AttachStdout,omitempty"` + Cmd []string `json:"Cmd,omitempty"` + Healthcheck *HealthConfig `json:"Healthcheck,omitempty"` + Domainname string `json:"Domainname,omitempty"` + Entrypoint []string `json:"Entrypoint,omitempty"` + Env []string `json:"Env,omitempty"` + Hostname string `json:"Hostname,omitempty"` + Image string `json:"Image,omitempty"` + Labels map[string]string `json:"Labels,omitempty"` + OnBuild []string `json:"OnBuild,omitempty"` + OpenStdin bool `json:"OpenStdin,omitempty"` + StdinOnce bool `json:"StdinOnce,omitempty"` + Tty bool `json:"Tty,omitempty"` + User string `json:"User,omitempty"` + Volumes map[string]struct{} `json:"Volumes,omitempty"` + WorkingDir string `json:"WorkingDir,omitempty"` + ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty"` + ArgsEscaped bool `json:"ArgsEscaped,omitempty"` + NetworkDisabled bool `json:"NetworkDisabled,omitempty"` + MacAddress string `json:"MacAddress,omitempty"` + StopSignal string `json:"StopSignal,omitempty"` + Shell []string `json:"Shell,omitempty"` +} + +type HealthConfig struct { + // Test is the test to perform to check that the container is healthy. + // An empty slice means to inherit the default. + // The options are: + // {} : inherit healthcheck + // {"NONE"} : disable healthcheck + // {"CMD", args...} : exec arguments directly + // {"CMD-SHELL", command} : run command with system's default shell + Test []string `json:",omitempty"` + + // Zero means to inherit. Durations are expressed as integer nanoseconds. + Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks. + Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung. + StartPeriod time.Duration `json:",omitempty"` // The start period for the container to initialize before the retries starts to count down. + + // Retries is the number of consecutive failures needed to consider a container as unhealthy. + // Zero means inherit. + Retries int `json:",omitempty"` +} diff --git a/pkg/domain/model/trivy/misconfiguration.go b/pkg/domain/model/trivy/misconfiguration.go new file mode 100644 index 00000000..ca8275ca --- /dev/null +++ b/pkg/domain/model/trivy/misconfiguration.go @@ -0,0 +1,62 @@ +package trivy + +type MisconfSummary struct { + Successes int + Failures int + Exceptions int +} + +type MisconfStatus string + +// DetectedMisconfiguration holds detected misconfigurations +type DetectedMisconfiguration struct { + Type string `json:",omitempty"` + ID string `json:",omitempty"` + AVDID string `json:",omitempty"` + Title string `json:",omitempty"` + Description string `json:",omitempty"` + Message string `json:",omitempty"` + Namespace string `json:",omitempty"` + Query string `json:",omitempty"` + Resolution string `json:",omitempty"` + Severity string `json:",omitempty"` + PrimaryURL string `json:",omitempty"` + References []string `json:",omitempty"` + Status MisconfStatus `json:",omitempty"` + Layer Layer `json:",omitempty"` + CauseMetadata CauseMetadata `json:",omitempty"` + + // For debugging + Traces []string `json:",omitempty"` +} + +type CauseMetadata struct { + Resource string `json:",omitempty"` + Provider string `json:",omitempty"` + Service string `json:",omitempty"` + StartLine int `json:",omitempty"` + EndLine int `json:",omitempty"` + Code Code `json:",omitempty"` + Occurrences []Occurrence `json:",omitempty"` +} + +type Occurrence struct { + Resource string `json:",omitempty"` + Filename string `json:",omitempty"` + Location Location +} + +type Code struct { + Lines []Line +} + +type Line struct { + Number int `json:"Number"` + Content string `json:"Content"` + IsCause bool `json:"IsCause"` + Annotation string `json:"Annotation"` + Truncated bool `json:"Truncated"` + Highlighted string `json:"Highlighted,omitempty"` + FirstCause bool `json:"FirstCause"` + LastCause bool `json:"LastCause"` +} diff --git a/pkg/domain/model/trivy/report.go b/pkg/domain/model/trivy/report.go new file mode 100644 index 00000000..00ae0693 --- /dev/null +++ b/pkg/domain/model/trivy/report.go @@ -0,0 +1,118 @@ +package trivy + +import ( + "github.com/m-mizutani/goerr" + "github.com/m-mizutani/octovy/pkg/domain/types" +) + +type Report struct { + SchemaVersion int `json:",omitempty"` + ArtifactName string `json:",omitempty"` + ArtifactType ArtifactType `json:",omitempty"` + Metadata Metadata `json:",omitempty"` + Results Results `json:",omitempty"` +} + +// Validate checks the required fields are filled. Currently, it checks only schema version. +func (x *Report) Validate() error { + if x.SchemaVersion == 0 { + return goerr.Wrap(types.ErrValidationFailed, "schema version is empty") + } + return nil +} + +// Metadata represents a metadata of artifact +type Metadata struct { + Size int64 `json:",omitempty"` + OS *OS `json:",omitempty"` + + // Container image + ImageID string `json:",omitempty"` + DiffIDs []string `json:",omitempty"` + RepoTags []string `json:",omitempty"` + RepoDigests []string `json:",omitempty"` + ImageConfig ConfigFile `json:",omitempty"` +} + +type Results []Result + +type Result struct { + Target string `json:"Target"` + Class ResultClass `json:"Class,omitempty"` + Type string `json:"Type,omitempty"` + Packages []Package `json:"Packages,omitempty"` + Vulnerabilities []DetectedVulnerability `json:"Vulnerabilities,omitempty"` + MisconfSummary *MisconfSummary `json:"MisconfSummary,omitempty"` + Misconfigurations []DetectedMisconfiguration `json:"Misconfigurations,omitempty"` + Secrets []SecretFinding `json:"Secrets,omitempty"` + Licenses []DetectedLicense `json:"Licenses,omitempty"` + // CustomResources []ftypes.CustomResource `json:"CustomResources,omitempty"` +} + +type ResultClass string +type Compliance = string +type Format string +type ArtifactType string +type Digest string + +type Status int + +type Repository struct { + Family string `json:",omitempty"` + Release string `json:",omitempty"` +} + +type Layer struct { + Digest string `json:",omitempty"` + DiffID string `json:",omitempty"` + CreatedBy string `json:",omitempty"` +} + +type Package struct { + ID string `json:",omitempty"` + Name string `json:",omitempty"` + Version string `json:",omitempty"` + Release string `json:",omitempty"` + Epoch int `json:",omitempty"` + Arch string `json:",omitempty"` + Dev bool `json:",omitempty"` + SrcName string `json:",omitempty"` + SrcVersion string `json:",omitempty"` + SrcRelease string `json:",omitempty"` + SrcEpoch int `json:",omitempty"` + Licenses []string `json:",omitempty"` + Maintainer string `json:",omitempty"` + + Modularitylabel string `json:",omitempty"` // only for Red Hat based distributions + BuildInfo *BuildInfo `json:",omitempty"` // only for Red Hat + + Ref string `json:",omitempty"` // identifier which can be used to reference the component elsewhere + Indirect bool `json:",omitempty"` // this package is direct dependency of the project or not + + // Dependencies of this package + // Note: it may have interdependencies, which may lead to infinite loops. + DependsOn []string `json:",omitempty"` + + Layer Layer `json:",omitempty"` + + // Each package metadata have the file path, while the package from lock files does not have. + FilePath string `json:",omitempty"` + + // This is required when using SPDX formats. Otherwise, it will be empty. + Digest Digest `json:",omitempty"` + + // lines from the lock file where the dependency is written + Locations []Location `json:",omitempty"` +} + +type Location struct { + StartLine int `json:",omitempty"` + EndLine int `json:",omitempty"` +} + +// BuildInfo represents information under /root/buildinfo in RHEL +type BuildInfo struct { + ContentSets []string `json:",omitempty"` + Nvr string `json:",omitempty"` + Arch string `json:",omitempty"` +} diff --git a/pkg/domain/model/trivy/secret_finding.go b/pkg/domain/model/trivy/secret_finding.go new file mode 100644 index 00000000..6a9e814d --- /dev/null +++ b/pkg/domain/model/trivy/secret_finding.go @@ -0,0 +1,20 @@ +package trivy + +type SecretRuleCategory string + +type Secret struct { + FilePath string + Findings []SecretFinding +} + +type SecretFinding struct { + RuleID string + Category SecretRuleCategory + Severity string + Title string + StartLine int + EndLine int + Code Code + Match string + Layer Layer `json:",omitempty"` +} diff --git a/pkg/domain/model/trivy/vulnerability.go b/pkg/domain/model/trivy/vulnerability.go new file mode 100644 index 00000000..4159d25f --- /dev/null +++ b/pkg/domain/model/trivy/vulnerability.go @@ -0,0 +1,95 @@ +package trivy + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" +) + +// DetectedVulnerability holds the information of detected vulnerabilities +type DetectedVulnerability struct { + VulnerabilityID string `json:",omitempty"` + VendorIDs []string `json:",omitempty"` + PkgID string `json:",omitempty"` // It is used to construct dependency graph. + PkgName string `json:",omitempty"` + PkgPath string `json:",omitempty"` // This field is populated in the case of language-specific packages such as egg/wheel and gemspec + InstalledVersion string `json:",omitempty"` + FixedVersion string `json:",omitempty"` + Status string `json:",omitempty"` + Layer Layer `json:",omitempty"` + SeveritySource SourceID `json:",omitempty"` + PrimaryURL string `json:",omitempty"` + + // PkgRef is populated only when scanning SBOM and contains the reference ID used in the SBOM. + // It could be PURL, UUID, etc. + // e.g. + // - pkg:npm/acme/component@1.0.0 + // - b2a46a4b-8367-4bae-9820-95557cfe03a8 + PkgRef string `json:",omitempty"` + + // DataSource holds where the advisory comes from + DataSource *DataSource `json:",omitempty"` + + // Custom is for extensibility and not supposed to be used in OSS + Custom interface{} `json:",omitempty"` + + // Embed vulnerability details + Vulnerability +} + +func (x *DetectedVulnerability) ID() string { + raw := bytes.Join([][]byte{ + []byte(x.VulnerabilityID), + []byte(x.PkgName), + []byte(x.PkgPath), + []byte(x.PkgID), + }, []byte{0x00}) + + h := sha256.New() + h.Write(raw) + return hex.EncodeToString((h.Sum(nil))) +} + +type SourceID string + +type DataSource struct { + ID SourceID `json:",omitempty"` + Name string `json:",omitempty"` + URL string `json:",omitempty"` +} + +type Vulnerability struct { + Title string `json:",omitempty"` + Description string `json:",omitempty"` + Severity string `json:",omitempty"` // Selected from VendorSeverity, depending on a scan target + CweIDs []string `json:",omitempty"` // e.g. CWE-78, CWE-89 + + // VenderSeverity is map and it may contain a field name with "-". It's not allowed in BigQuery and excluded. + // VendorSeverity VendorSeverity `json:",omitempty"` + + CVSS VendorCVSS `json:",omitempty"` + References []string `json:",omitempty"` + PublishedDate string `json:",omitempty"` // Take from NVD + LastModifiedDate string `json:",omitempty"` // Take from NVD + + // Custom is basically for extensibility and is not supposed to be used in OSS + Custom interface{} `json:",omitempty"` +} + +type Severity int + +// type VendorSeverity map[SourceID]Severity + +type CVSS struct { + V2Vector string `json:"V2Vector,omitempty"` + V3Vector string `json:"V3Vector,omitempty"` + V2Score float64 `json:"V2Score,omitempty"` + V3Score float64 `json:"V3Score,omitempty"` +} + +type CVSSVector struct { + V2 string `json:"v2,omitempty"` + V3 string `json:"v3,omitempty"` +} + +type VendorCVSS map[SourceID]CVSS diff --git a/pkg/domain/model/usecase.go b/pkg/domain/model/usecase.go new file mode 100644 index 00000000..d8cf3ef4 --- /dev/null +++ b/pkg/domain/model/usecase.go @@ -0,0 +1,22 @@ +package model + +import ( + "github.com/m-mizutani/goerr" + "github.com/m-mizutani/octovy/pkg/domain/types" +) + +type ScanGitHubRepoInput struct { + GitHubMetadata + InstallID types.GitHubAppInstallID +} + +func (x *ScanGitHubRepoInput) Validate() error { + if err := x.GitHubMetadata.Validate(); err != nil { + return err + } + if x.InstallID == 0 { + return goerr.Wrap(types.ErrInvalidOption, "install ID is empty") + } + + return nil +} diff --git a/pkg/domain/types/const.go b/pkg/domain/types/const.go new file mode 100644 index 00000000..d779be98 --- /dev/null +++ b/pkg/domain/types/const.go @@ -0,0 +1,5 @@ +package types + +const ( + GitHubCommentSignature = "" +) diff --git a/pkg/domain/types/error.go b/pkg/domain/types/error.go index 3ecd0a7e..b935bd61 100644 --- a/pkg/domain/types/error.go +++ b/pkg/domain/types/error.go @@ -1,13 +1,20 @@ package types -import "github.com/m-mizutani/goerr" +import "errors" var ( - ErrInvalidOption = goerr.New("invalid option") + // ErrInvalidOption is an error that indicates an invalid option is given by user via CLI or configuration + ErrInvalidOption = errors.New("invalid option") - ErrInvalidRequest = goerr.New("invalid request") + // ErrInvalidRequest is an error that indicates an invalid HTTP request + ErrInvalidRequest = errors.New("invalid request") - ErrInvalidGitHubData = goerr.New("invalid GitHub data") + // ErrInvalidResponse is an error that indicates a failure in data consistency in the application + ErrValidationFailed = errors.New("validation failed") - ErrLogicError = goerr.New("logic error") + // ErrInvalidGitHubData is an error that indicates an invalid data provided by GitHub. Mainly used in GitHub API response + ErrInvalidGitHubData = errors.New("invalid GitHub data") + + // ErrLogicError is an error that indicates a logic error in the application + ErrLogicError = errors.New("logic error") ) diff --git a/pkg/domain/types/types.go b/pkg/domain/types/types.go index 43013b12..6af37b12 100644 --- a/pkg/domain/types/types.go +++ b/pkg/domain/types/types.go @@ -1,7 +1,26 @@ package types +import "github.com/google/uuid" + type ( ScanID string + RequestID string +) + +func NewScanID() ScanID { return ScanID(uuid.NewString()) } +func (x ScanID) String() string { return string(x) } + +func NewRequestID() RequestID { return RequestID(uuid.NewString()) } +func (x RequestID) String() string { return string(x) } + +type ( GoogleProjectID string + + BQDatasetID string + BQTableID string ) + +func (x GoogleProjectID) String() string { return string(x) } +func (x BQDatasetID) String() string { return string(x) } +func (x BQTableID) String() string { return string(x) } diff --git a/pkg/infra/bq/client.go b/pkg/infra/bq/client.go new file mode 100644 index 00000000..6a783da2 --- /dev/null +++ b/pkg/infra/bq/client.go @@ -0,0 +1,146 @@ +package bq + +import ( + "context" + "encoding/json" + + "cloud.google.com/go/bigquery" + "cloud.google.com/go/bigquery/storage/managedwriter" + "cloud.google.com/go/bigquery/storage/managedwriter/adapt" + "github.com/m-mizutani/goerr" + "github.com/m-mizutani/octovy/pkg/domain/interfaces" + "github.com/m-mizutani/octovy/pkg/domain/types" + "github.com/m-mizutani/octovy/pkg/utils" + "google.golang.org/api/googleapi" + "google.golang.org/api/option" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/dynamicpb" +) + +type Client struct { + bqClient *bigquery.Client + mwClient *managedwriter.Client + project string + dataset string +} + +var _ interfaces.BigQuery = (*Client)(nil) + +func New(ctx context.Context, projectID types.GoogleProjectID, datasetID types.BQDatasetID, options ...option.ClientOption) (*Client, error) { + mwClient, err := managedwriter.NewClient(ctx, projectID.String(), options...) + if err != nil { + return nil, goerr.Wrap(err, "failed to create bigquery client").With("projectID", projectID) + } + + bqClient, err := bigquery.NewClient(ctx, string(projectID), options...) + if err != nil { + return nil, goerr.Wrap(err, "failed to create BigQuery client").With("projectID", projectID) + } + + return &Client{ + bqClient: bqClient, + mwClient: mwClient, + project: projectID.String(), + dataset: datasetID.String(), + }, nil +} + +// CreateTable implements interfaces.BigQuery. +func (x *Client) CreateTable(ctx context.Context, table types.BQTableID, md *bigquery.TableMetadata) error { + if err := x.bqClient.Dataset(x.dataset).Table(table.String()).Create(ctx, md); err != nil { + return goerr.Wrap(err, "failed to create table").With("dataset", x.dataset).With("table", table) + } + return nil +} + +// GetMetadata implements interfaces.BigQuery. If the table does not exist, it returns nil. +func (x *Client) GetMetadata(ctx context.Context, table types.BQTableID) (*bigquery.TableMetadata, error) { + md, err := x.bqClient.Dataset(x.dataset).Table(table.String()).Metadata(ctx) + if err != nil { + if gErr, ok := err.(*googleapi.Error); ok && gErr.Code == 404 { + return nil, nil + } + return nil, goerr.Wrap(err, "failed to get table metadata").With("dataset", x.dataset).With("table", table) + } + + return md, nil +} + +// Insert implements interfaces.BigQuery. +func (x *Client) Insert(ctx context.Context, table types.BQTableID, schema bigquery.Schema, data any) error { + convertedSchema, err := adapt.BQSchemaToStorageTableSchema(schema) + if err != nil { + return goerr.Wrap(err, "failed to convert schema") + } + + descriptor, err := adapt.StorageSchemaToProto2Descriptor(convertedSchema, "root") + if err != nil { + return goerr.Wrap(err, "failed to convert schema to descriptor") + } + messageDescriptor, ok := descriptor.(protoreflect.MessageDescriptor) + if !ok { + return goerr.Wrap(err, "adapted descriptor is not a message descriptor") + } + descriptorProto, err := adapt.NormalizeDescriptor(messageDescriptor) + if err != nil { + return goerr.Wrap(err, "failed to normalize descriptor") + } + + message := dynamicpb.NewMessage(messageDescriptor) + + raw, err := json.Marshal(data) + if err != nil { + return goerr.Wrap(err, "failed to Marshal json message").With("v", data) + } + + // First, json->proto message + err = protojson.Unmarshal(raw, message) + if err != nil { + return goerr.Wrap(err, "failed to Unmarshal json message").With("raw", string(raw)) + } + // Then, proto message -> bytes. + b, err := proto.Marshal(message) + if err != nil { + return goerr.Wrap(err, "failed to Marshal proto message") + } + + rows := [][]byte{b} + + ms, err := x.mwClient.NewManagedStream(ctx, + managedwriter.WithDestinationTable( + managedwriter.TableParentFromParts( + x.project, + x.dataset, + table.String(), + ), + ), + // managedwriter.WithType(managedwriter.CommittedStream), + managedwriter.WithSchemaDescriptor(descriptorProto), + ) + if err != nil { + return goerr.Wrap(err, "failed to create managed stream") + } + defer utils.SafeClose(ms) + + arResult, err := ms.AppendRows(ctx, rows) + if err != nil { + return goerr.Wrap(err, "failed to append rows") + } + + if _, err := arResult.FullResponse(ctx); err != nil { + return goerr.Wrap(err, "failed to get append result") + } + + return nil +} + +// UpdateTable implements interfaces.BigQuery. +func (x *Client) UpdateTable(ctx context.Context, table types.BQTableID, md bigquery.TableMetadataToUpdate, eTag string) error { + if _, err := x.bqClient.Dataset(x.dataset).Table(table.String()).Update(ctx, md, eTag); err != nil { + return goerr.Wrap(err, "failed to update table").With("dataset", x.dataset).With("table", table).With("meta", md) + } + + return nil +} diff --git a/pkg/infra/bq/client_test.go b/pkg/infra/bq/client_test.go new file mode 100644 index 00000000..f2f001ad --- /dev/null +++ b/pkg/infra/bq/client_test.go @@ -0,0 +1,96 @@ +package bq_test + +import ( + "context" + "testing" + "time" + + "cloud.google.com/go/bigquery" + "github.com/m-mizutani/bqs" + "github.com/m-mizutani/gt" + "github.com/m-mizutani/octovy/pkg/domain/model" + "github.com/m-mizutani/octovy/pkg/domain/types" + "github.com/m-mizutani/octovy/pkg/infra/bq" + "github.com/m-mizutani/octovy/pkg/utils" + "google.golang.org/api/impersonate" + "google.golang.org/api/option" +) + +func TestClient(t *testing.T) { + projectID := utils.LoadEnv(t, "TEST_BIGQUERY_PROJECT_ID") + datasetID := utils.LoadEnv(t, "TEST_BIGQUERY_DATASET_ID") + + ctx := context.Background() + + tblName := types.BQTableID(time.Now().Format("insert_test_20060102_150405")) + client, err := bq.New(ctx, types.GoogleProjectID(projectID), types.BQDatasetID(datasetID)) + gt.NoError(t, err) + + var baseSchema bigquery.Schema + + t.Run("Create base table at first", func(t *testing.T) { + var scan model.Scan + baseSchema = gt.R1(bqs.Infer(scan)).NoError(t) + gt.NoError(t, err) + + gt.NoError(t, client.CreateTable(ctx, tblName, &bigquery.TableMetadata{ + Name: tblName.String(), + Schema: baseSchema, + })) + }) + + t.Run("Insert record", func(t *testing.T) { + var scan model.Scan + utils.LoadJson(t, "testdata/data.json", &scan.Report) + dataSchema := gt.R1(bqs.Infer(scan)).NoError(t) + mergedSchema := gt.R1(bqs.Merge(baseSchema, dataSchema)).NoError(t) + + md := gt.R1(client.GetMetadata(ctx, tblName)).NoError(t) + gt.False(t, bqs.Equal(mergedSchema, baseSchema)) + gt.NoError(t, client.UpdateTable(ctx, tblName, bigquery.TableMetadataToUpdate{ + Schema: mergedSchema, + }, md.ETag)).Must() + + record := model.ScanRawRecord{ + Scan: scan, + Timestamp: scan.Timestamp.UnixMicro(), + } + gt.NoError(t, client.Insert(ctx, tblName, mergedSchema, record)) + }) +} + +func TestImpersonation(t *testing.T) { + projectID := utils.LoadEnv(t, "TEST_BIGQUERY_PROJECT_ID") + datasetID := utils.LoadEnv(t, "TEST_BIGQUERY_DATASET_ID") + serviceAccount := utils.LoadEnv(t, "TEST_BIGQUERY_IMPERSONATE_SERVICE_ACCOUNT") + + ctx := context.Background() + + ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{ + TargetPrincipal: serviceAccount, + Scopes: []string{ + "https://www.googleapis.com/auth/bigquery", + "https://www.googleapis.com/auth/cloud-platform", + }, + }) + gt.NoError(t, err) + + client, err := bq.New(ctx, types.GoogleProjectID(projectID), types.BQDatasetID(datasetID), option.WithTokenSource(ts)) + gt.NoError(t, err) + + msg := struct { + Msg string + }{ + Msg: "Hello, BigQuery: " + time.Now().String(), + } + + schema := gt.R1(bqs.Infer(msg)).NoError(t) + + tblName := types.BQTableID(time.Now().Format("impersonation_test_20060102_150405")) + gt.NoError(t, client.CreateTable(ctx, tblName, &bigquery.TableMetadata{ + Name: tblName.String(), + Schema: schema, + })) + + gt.NoError(t, client.Insert(ctx, tblName, schema, msg)) +} diff --git a/pkg/infra/bq/data.json b/pkg/infra/bq/data.json new file mode 100644 index 00000000..5894537c --- /dev/null +++ b/pkg/infra/bq/data.json @@ -0,0 +1 @@ +{"id":"","github":{"repo_id":0,"owner":"","repo_name":"","committer":{"id":0,"login":"","email":""},"commit_id":"","branch":"","ref":"","pull_request":null,"default_branch":""},"report":{"SchemaVersion":2,"ArtifactName":".","ArtifactType":"filesystem","Metadata":{"ImageConfig":{"architecture":"","created":"0001-01-01T00:00:00Z","os":"","rootfs":{"type":"","diff_ids":null},"config":{}}},"Results":[{"Target":"Gemfile.lock","Class":"lang-pkgs","Type":"bundler","Packages":[{"ID":"octovy-test@0.1.0","Name":"octovy-test","Version":"0.1.0","Indirect":true,"Layer":{},"Locations":[{"StartLine":4,"EndLine":4}]},{"ID":"rake@10.5.0","Name":"rake","Version":"10.5.0","Layer":{},"Locations":[{"StartLine":9,"EndLine":9}]}],"Vulnerabilities":[{"VulnerabilityID":"CVE-2020-8130","PkgID":"rake@10.5.0","PkgName":"rake","InstalledVersion":"10.5.0","FixedVersion":"\u003e= 12.3.3","Status":"fixed","Layer":{},"SeveritySource":"ruby-advisory-db","PrimaryURL":"https://avd.aquasec.com/nvd/cve-2020-8130","DataSource":{"ID":"ruby-advisory-db","Name":"Ruby Advisory Database","URL":"https://github.com/rubysec/ruby-advisory-db"},"Title":"rake: OS Command Injection via egrep in Rake::FileList","Description":"There is an OS command injection vulnerability in Ruby Rake \u003c 12.3.3 in Rake::FileList when supplying a filename that begins with the pipe character `|`.","Severity":"HIGH","CweIDs":["CWE-78"],"CVSS":{"ghsa":{"V3Vector":"CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H","V3Score":6.4},"nvd":{"V2Vector":"AV:L/AC:M/Au:N/C:C/I:C/A:C","V3Vector":"CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H","V2Score":6.9,"V3Score":6.4},"redhat":{"V3Vector":"CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H","V3Score":6.4}},"References":["http://lists.opensuse.org/opensuse-security-announce/2020-03/msg00041.html","https://access.redhat.com/security/cve/CVE-2020-8130","https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-8130","https://github.com/advisories/GHSA-jppv-gw3r-w3q8","https://github.com/ruby/rake","https://github.com/ruby/rake/commit/5b8f8fc41a5d7d7d6a5d767e48464c60884d3aee","https://github.com/rubysec/ruby-advisory-db/blob/master/gems/rake/CVE-2020-8130.yml","https://hackerone.com/reports/651518","https://lists.debian.org/debian-lts-announce/2020/02/msg00026.html","https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/523CLQ62VRN3VVC52KMPTROCCKY4Z36B/","https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/VXMX4ARNX2JLRJMSH4N3J3UBMUT5CI44/","https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/523CLQ62VRN3VVC52KMPTROCCKY4Z36B","https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/VXMX4ARNX2JLRJMSH4N3J3UBMUT5CI44","https://nvd.nist.gov/vuln/detail/CVE-2020-8130","https://ubuntu.com/security/notices/USN-4295-1","https://usn.ubuntu.com/4295-1","https://usn.ubuntu.com/4295-1/","https://www.cve.org/CVERecord?id=CVE-2020-8130"],"PublishedDate":"2020-02-24T15:15:11.957Z","LastModifiedDate":"2023-11-07T03:26:16.5Z"}]}]},"timestamp":-62135596800000000} diff --git a/pkg/infra/bq/mock.go b/pkg/infra/bq/mock.go new file mode 100644 index 00000000..6fb13fdc --- /dev/null +++ b/pkg/infra/bq/mock.go @@ -0,0 +1,38 @@ +package bq + +import ( + "context" + + "cloud.google.com/go/bigquery" + "github.com/m-mizutani/octovy/pkg/domain/interfaces" + "github.com/m-mizutani/octovy/pkg/domain/types" +) + +type Mock struct { + FnCreateTable func(ctx context.Context, table types.BQTableID, md *bigquery.TableMetadata) error + FnGetMetadata func(ctx context.Context, table types.BQTableID) (*bigquery.TableMetadata, error) + FnInsert func(ctx context.Context, tableID types.BQTableID, schema bigquery.Schema, data any) error + FnUpdateTable func(ctx context.Context, table types.BQTableID, md bigquery.TableMetadataToUpdate, eTag string) error +} + +// CreateTable implements interfaces.BigQuery. +func (m *Mock) CreateTable(ctx context.Context, table types.BQTableID, md *bigquery.TableMetadata) error { + return m.FnCreateTable(ctx, table, md) +} + +// GetMetadata implements interfaces.BigQuery. +func (m *Mock) GetMetadata(ctx context.Context, table types.BQTableID) (*bigquery.TableMetadata, error) { + return m.FnGetMetadata(ctx, table) +} + +// Insert implements interfaces.BigQuery. +func (m *Mock) Insert(ctx context.Context, tableID types.BQTableID, schema bigquery.Schema, data any) error { + return m.FnInsert(ctx, tableID, schema, data) +} + +// UpdateTable implements interfaces.BigQuery. +func (m *Mock) UpdateTable(ctx context.Context, table types.BQTableID, md bigquery.TableMetadataToUpdate, eTag string) error { + return m.FnUpdateTable(ctx, table, md, eTag) +} + +var _ interfaces.BigQuery = &Mock{} diff --git a/pkg/infra/bq/testdata/data.json b/pkg/infra/bq/testdata/data.json new file mode 100644 index 00000000..3f4ed267 --- /dev/null +++ b/pkg/infra/bq/testdata/data.json @@ -0,0 +1,131 @@ +{ + "SchemaVersion": 2, + "CreatedAt": "2024-04-13T10:20:43.10296+09:00", + "ArtifactName": ".", + "ArtifactType": "filesystem", + "Metadata": { + "ImageConfig": { + "architecture": "", + "created": "0001-01-01T00:00:00Z", + "os": "", + "rootfs": { + "type": "", + "diff_ids": null + }, + "config": {} + } + }, + "Results": [ + { + "Target": "Gemfile.lock", + "Class": "lang-pkgs", + "Type": "bundler", + "Packages": [ + { + "ID": "octovy-test@0.1.0", + "Name": "octovy-test", + "Identifier": { + "PURL": "pkg:gem/octovy-test@0.1.0" + }, + "Version": "0.1.0", + "Indirect": true, + "Layer": {}, + "Locations": [ + { + "StartLine": 4, + "EndLine": 4 + } + ] + }, + { + "ID": "rake@10.5.0", + "Name": "rake", + "Identifier": { + "PURL": "pkg:gem/rake@10.5.0" + }, + "Version": "10.5.0", + "Layer": {}, + "Locations": [ + { + "StartLine": 9, + "EndLine": 9 + } + ] + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2020-8130", + "PkgID": "rake@10.5.0", + "PkgName": "rake", + "PkgIdentifier": { + "PURL": "pkg:gem/rake@10.5.0" + }, + "InstalledVersion": "10.5.0", + "FixedVersion": "\u003e= 12.3.3", + "Status": "fixed", + "Layer": {}, + "SeveritySource": "ruby-advisory-db", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-8130", + "DataSource": { + "ID": "ruby-advisory-db", + "Name": "Ruby Advisory Database", + "URL": "https://github.com/rubysec/ruby-advisory-db" + }, + "Title": "rake: OS Command Injection via egrep in Rake::FileList", + "Description": "There is an OS command injection vulnerability in Ruby Rake \u003c 12.3.3 in Rake::FileList when supplying a filename that begins with the pipe character `|`.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-78" + ], + "VendorSeverity": { + "amazon": 2, + "ghsa": 2, + "nvd": 2, + "redhat": 2, + "ruby-advisory-db": 3, + "ubuntu": 2 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 6.4 + }, + "nvd": { + "V2Vector": "AV:L/AC:M/Au:N/C:C/I:C/A:C", + "V3Vector": "CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 6.9, + "V3Score": 6.4 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 6.4 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2020-03/msg00041.html", + "https://access.redhat.com/security/cve/CVE-2020-8130", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-8130", + "https://github.com/advisories/GHSA-jppv-gw3r-w3q8", + "https://github.com/ruby/rake", + "https://github.com/ruby/rake/commit/5b8f8fc41a5d7d7d6a5d767e48464c60884d3aee", + "https://github.com/rubysec/ruby-advisory-db/blob/master/gems/rake/CVE-2020-8130.yml", + "https://hackerone.com/reports/651518", + "https://lists.debian.org/debian-lts-announce/2020/02/msg00026.html", + "https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/523CLQ62VRN3VVC52KMPTROCCKY4Z36B/", + "https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/VXMX4ARNX2JLRJMSH4N3J3UBMUT5CI44/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/523CLQ62VRN3VVC52KMPTROCCKY4Z36B", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/VXMX4ARNX2JLRJMSH4N3J3UBMUT5CI44", + "https://nvd.nist.gov/vuln/detail/CVE-2020-8130", + "https://ubuntu.com/security/notices/USN-4295-1", + "https://usn.ubuntu.com/4295-1", + "https://usn.ubuntu.com/4295-1/", + "https://www.cve.org/CVERecord?id=CVE-2020-8130" + ], + "PublishedDate": "2020-02-24T15:15:11.957Z", + "LastModifiedDate": "2023-11-07T03:26:16.5Z" + } + ] + } + ] +} diff --git a/pkg/infra/clients.go b/pkg/infra/clients.go index 860fef16..4551806f 100644 --- a/pkg/infra/clients.go +++ b/pkg/infra/clients.go @@ -1,18 +1,19 @@ package infra import ( - "database/sql" "net/http" - gh "github.com/m-mizutani/octovy/pkg/infra/gh" + "github.com/m-mizutani/octovy/pkg/domain/interfaces" "github.com/m-mizutani/octovy/pkg/infra/trivy" ) type Clients struct { - githubApp gh.Client + githubApp interfaces.GitHub httpClient HTTPClient trivyClient trivy.Client - dbClient *sql.DB + bqClient interfaces.BigQuery + storage interfaces.Storage + policy interfaces.Policy } type HTTPClient interface { @@ -34,7 +35,7 @@ func New(options ...Option) *Clients { return client } -func (x *Clients) GitHubApp() gh.Client { +func (x *Clients) GitHubApp() interfaces.GitHub { return x.githubApp } func (x *Clients) HTTPClient() HTTPClient { @@ -43,11 +44,17 @@ func (x *Clients) HTTPClient() HTTPClient { func (x *Clients) Trivy() trivy.Client { return x.trivyClient } -func (x *Clients) DB() *sql.DB { - return x.dbClient +func (x *Clients) BigQuery() interfaces.BigQuery { + return x.bqClient +} +func (x *Clients) Storage() interfaces.Storage { + return x.storage +} +func (x *Clients) Policy() interfaces.Policy { + return x.policy } -func WithGitHubApp(client gh.Client) Option { +func WithGitHubApp(client interfaces.GitHub) Option { return func(x *Clients) { x.githubApp = client } @@ -65,8 +72,20 @@ func WithTrivy(client trivy.Client) Option { } } -func WithDB(client *sql.DB) Option { +func WithBigQuery(client interfaces.BigQuery) Option { + return func(x *Clients) { + x.bqClient = client + } +} + +func WithStorage(client interfaces.Storage) Option { + return func(x *Clients) { + x.storage = client + } +} + +func WithPolicy(client interfaces.Policy) Option { return func(x *Clients) { - x.dbClient = client + x.policy = client } } diff --git a/pkg/infra/cs/client.go b/pkg/infra/cs/client.go new file mode 100644 index 00000000..5e5e33b5 --- /dev/null +++ b/pkg/infra/cs/client.go @@ -0,0 +1,78 @@ +// Google Cloud Storage client +package cs + +import ( + "compress/gzip" + "context" + "io" + + "cloud.google.com/go/storage" + "github.com/m-mizutani/goerr" + "github.com/m-mizutani/octovy/pkg/domain/interfaces" + "github.com/m-mizutani/octovy/pkg/utils" +) + +type Client struct { + bucket string + prefix string + client *storage.Client +} + +var _ interfaces.Storage = (*Client)(nil) + +func New(ctx context.Context, bucket string, options ...Option) (*Client, error) { + client, err := storage.NewClient(ctx) + if err != nil { + return nil, err + } + + return &Client{ + bucket: bucket, + client: client, + }, nil +} + +// Option is a functional option for New function +type Option func(*Client) + +func WithPrefix(prefix string) Option { + return func(c *Client) { + c.prefix = prefix + } +} + +// Get implements interfaces.Storage. +func (c *Client) Get(ctx context.Context, key string) (io.ReadCloser, error) { + obj := c.client.Bucket(c.bucket).Object(c.prefix + key) + r, err := obj.NewReader(ctx) + if err != nil { + // check if the object does not exist + if err == storage.ErrObjectNotExist { + return nil, nil + } + return nil, goerr.Wrap(err, "Failed to create object reader") + } + + return r, nil +} + +// Put implements interfaces.Storage. +func (c *Client) Put(ctx context.Context, key string, r io.ReadCloser) error { + obj := c.client.Bucket(c.bucket).Object(c.prefix + key) + w := obj.NewWriter(ctx) + w.ContentType = "application/json" + w.ContentEncoding = "gzip" + + zw := gzip.NewWriter(w) + + defer func() { + utils.SafeClose(zw) + utils.SafeClose(w) + }() + + if _, err := io.Copy(zw, r); err != nil { + return goerr.Wrap(err, "Failed to write object") + } + + return nil +} diff --git a/pkg/infra/cs/client_test.go b/pkg/infra/cs/client_test.go new file mode 100644 index 00000000..3297a026 --- /dev/null +++ b/pkg/infra/cs/client_test.go @@ -0,0 +1,35 @@ +package cs_test + +import ( + "context" + "io" + "strings" + "testing" + + "github.com/google/uuid" + "github.com/m-mizutani/gt" + "github.com/m-mizutani/octovy/pkg/infra/cs" + "github.com/m-mizutani/octovy/pkg/utils" +) + +func TestCloudStorage(t *testing.T) { + bucket := utils.LoadEnv(t, "TEST_CLOUD_STORAGE_BUCKET") + + t.Run("Put and Get", func(t *testing.T) { + client, err := cs.New(context.Background(), bucket) + gt.NoError(t, err) + + key := "test-key/" + uuid.NewString() + ".txt" + r := strings.NewReader("blue") + + gt.NoError(t, client.Put(context.Background(), key, io.NopCloser(r))) + + r2, err := client.Get(context.Background(), key) + gt.NoError(t, err) + defer r2.Close() + + data, err := io.ReadAll(r2) + gt.NoError(t, err) + gt.Equal(t, "blue", string(data)) + }) +} diff --git a/pkg/infra/db/db.go b/pkg/infra/db/db.go deleted file mode 100644 index 46fda544..00000000 --- a/pkg/infra/db/db.go +++ /dev/null @@ -1,31 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.21.0 - -package db - -import ( - "context" - "database/sql" -) - -type DBTX interface { - ExecContext(context.Context, string, ...interface{}) (sql.Result, error) - PrepareContext(context.Context, string) (*sql.Stmt, error) - QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) - QueryRowContext(context.Context, string, ...interface{}) *sql.Row -} - -func New(db DBTX) *Queries { - return &Queries{db: db} -} - -type Queries struct { - db DBTX -} - -func (q *Queries) WithTx(tx *sql.Tx) *Queries { - return &Queries{ - db: tx, - } -} diff --git a/pkg/infra/db/models.go b/pkg/infra/db/models.go deleted file mode 100644 index 7eac0f42..00000000 --- a/pkg/infra/db/models.go +++ /dev/null @@ -1,126 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.21.0 - -package db - -import ( - "database/sql" - "database/sql/driver" - "fmt" - "time" - - "github.com/google/uuid" - "github.com/sqlc-dev/pqtype" -) - -type TargetClass string - -const ( - TargetClassOsPkgs TargetClass = "os-pkgs" - TargetClassLangPkgs TargetClass = "lang-pkgs" -) - -func (e *TargetClass) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *e = TargetClass(s) - case string: - *e = TargetClass(s) - default: - return fmt.Errorf("unsupported scan type for TargetClass: %T", src) - } - return nil -} - -type NullTargetClass struct { - TargetClass TargetClass - Valid bool // Valid is true if TargetClass is not NULL -} - -// Scan implements the Scanner interface. -func (ns *NullTargetClass) Scan(value interface{}) error { - if value == nil { - ns.TargetClass, ns.Valid = "", false - return nil - } - ns.Valid = true - return ns.TargetClass.Scan(value) -} - -// Value implements the driver Valuer interface. -func (ns NullTargetClass) Value() (driver.Value, error) { - if !ns.Valid { - return nil, nil - } - return string(ns.TargetClass), nil -} - -type DetectedPackage struct { - ID uuid.UUID - ResultID uuid.UUID - PkgID string -} - -type DetectedVulnerability struct { - ID uuid.UUID - ResultID uuid.UUID - VulnID string - PkgID string - FixedVersion sql.NullString - InstalledVersion sql.NullString - Data pqtype.NullRawMessage -} - -type GithubRepository struct { - ID uuid.UUID - RepoID int64 - Owner string - RepoName string - PageSeq sql.NullInt32 -} - -type MetaGithubRepository struct { - ID uuid.UUID - ScanID uuid.UUID - RepositoryID uuid.UUID - CommitID string - Branch sql.NullString - IsDefaultBranch sql.NullBool - BaseCommitID sql.NullString - PullRequestID sql.NullInt32 - PageSeq sql.NullInt32 -} - -type Package struct { - ID string - TargetType string - Name string - Version string -} - -type Result struct { - ID uuid.UUID - ScanID uuid.UUID - Target string - TargetType string - Class TargetClass -} - -type Scan struct { - ID uuid.UUID - CreatedAt time.Time - ArtifactName string - ArtifactType string - PageSeq sql.NullInt32 -} - -type Vulnerability struct { - ID string - Title string - Severity string - PublishedAt sql.NullTime - LastModifiedAt sql.NullTime - Data pqtype.NullRawMessage - PageSeq sql.NullInt32 -} diff --git a/pkg/infra/db/query.sql.go b/pkg/infra/db/query.sql.go deleted file mode 100644 index 9f4116e7..00000000 --- a/pkg/infra/db/query.sql.go +++ /dev/null @@ -1,439 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.21.0 -// source: query.sql - -package db - -import ( - "context" - "database/sql" - - "github.com/google/uuid" - "github.com/lib/pq" - "github.com/sqlc-dev/pqtype" -) - -const getLatestResultsByCommit = `-- name: GetLatestResultsByCommit :many -SELECT results.id, results.scan_id, results.target, results.target_type, results.class FROM results -INNER JOIN ( - SELECT scans.id AS id FROM meta_github_repository - INNER JOIN scans ON scans.id = meta_github_repository.scan_id - INNER JOIN github_repository ON github_repository.id = meta_github_repository.repository_id - WHERE meta_github_repository.commit_id = $1 - AND github_repository.repo_id = $2 - ORDER BY scans.created_at DESC - LIMIT 1 -) AS latest_scan ON latest_scan.id = results.scan_id -` - -type GetLatestResultsByCommitParams struct { - CommitID string - RepoID int64 -} - -func (q *Queries) GetLatestResultsByCommit(ctx context.Context, arg GetLatestResultsByCommitParams) ([]Result, error) { - rows, err := q.db.QueryContext(ctx, getLatestResultsByCommit, arg.CommitID, arg.RepoID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Result - for rows.Next() { - var i Result - if err := rows.Scan( - &i.ID, - &i.ScanID, - &i.Target, - &i.TargetType, - &i.Class, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getPackages = `-- name: GetPackages :many -SELECT id, target_type, name, version FROM packages WHERE id = ANY($1::text[]) -` - -func (q *Queries) GetPackages(ctx context.Context, dollar_1 []string) ([]Package, error) { - rows, err := q.db.QueryContext(ctx, getPackages, pq.Array(dollar_1)) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Package - for rows.Next() { - var i Package - if err := rows.Scan( - &i.ID, - &i.TargetType, - &i.Name, - &i.Version, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getVulnerabilities = `-- name: GetVulnerabilities :many -SELECT id, title, severity, published_at, last_modified_at, data, page_seq FROM vulnerabilities WHERE id = ANY($1::text[]) -` - -func (q *Queries) GetVulnerabilities(ctx context.Context, dollar_1 []string) ([]Vulnerability, error) { - rows, err := q.db.QueryContext(ctx, getVulnerabilities, pq.Array(dollar_1)) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Vulnerability - for rows.Next() { - var i Vulnerability - if err := rows.Scan( - &i.ID, - &i.Title, - &i.Severity, - &i.PublishedAt, - &i.LastModifiedAt, - &i.Data, - &i.PageSeq, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getVulnerabilitiesByResultID = `-- name: GetVulnerabilitiesByResultID :many -SELECT detected_vulnerabilities.id, detected_vulnerabilities.result_id, detected_vulnerabilities.vuln_id, detected_vulnerabilities.pkg_id, detected_vulnerabilities.fixed_version, detected_vulnerabilities.installed_version, detected_vulnerabilities.data FROM detected_vulnerabilities -WHERE detected_vulnerabilities.result_id = $1 -` - -func (q *Queries) GetVulnerabilitiesByResultID(ctx context.Context, resultID uuid.UUID) ([]DetectedVulnerability, error) { - rows, err := q.db.QueryContext(ctx, getVulnerabilitiesByResultID, resultID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []DetectedVulnerability - for rows.Next() { - var i DetectedVulnerability - if err := rows.Scan( - &i.ID, - &i.ResultID, - &i.VulnID, - &i.PkgID, - &i.FixedVersion, - &i.InstalledVersion, - &i.Data, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getVulnerability = `-- name: GetVulnerability :one -SELECT id, title, severity, published_at, last_modified_at, data, page_seq FROM vulnerabilities WHERE id = $1 -` - -func (q *Queries) GetVulnerability(ctx context.Context, id string) (Vulnerability, error) { - row := q.db.QueryRowContext(ctx, getVulnerability, id) - var i Vulnerability - err := row.Scan( - &i.ID, - &i.Title, - &i.Severity, - &i.PublishedAt, - &i.LastModifiedAt, - &i.Data, - &i.PageSeq, - ) - return i, err -} - -const saveDetectedPackage = `-- name: SaveDetectedPackage :exec -INSERT INTO detected_packages ( - id, - result_id, - pkg_id -) VALUES ( - $1, $2, $3 -) -` - -type SaveDetectedPackageParams struct { - ID uuid.UUID - ResultID uuid.UUID - PkgID string -} - -func (q *Queries) SaveDetectedPackage(ctx context.Context, arg SaveDetectedPackageParams) error { - _, err := q.db.ExecContext(ctx, saveDetectedPackage, arg.ID, arg.ResultID, arg.PkgID) - return err -} - -const saveDetectedVulnerability = `-- name: SaveDetectedVulnerability :exec -INSERT INTO detected_vulnerabilities ( - id, - result_id, - vuln_id, - pkg_id, - installed_version, - fixed_version, - data -) VALUES ( - $1, $2, $3, $4, $5, $6, $7 -) -` - -type SaveDetectedVulnerabilityParams struct { - ID uuid.UUID - ResultID uuid.UUID - VulnID string - PkgID string - InstalledVersion sql.NullString - FixedVersion sql.NullString - Data pqtype.NullRawMessage -} - -func (q *Queries) SaveDetectedVulnerability(ctx context.Context, arg SaveDetectedVulnerabilityParams) error { - _, err := q.db.ExecContext(ctx, saveDetectedVulnerability, - arg.ID, - arg.ResultID, - arg.VulnID, - arg.PkgID, - arg.InstalledVersion, - arg.FixedVersion, - arg.Data, - ) - return err -} - -const saveGithubRepository = `-- name: SaveGithubRepository :one -WITH ins AS ( - INSERT INTO github_repository ( - id, - repo_id, - owner, - repo_name - ) VALUES ( - $1, $2, $3, $4 - ) ON CONFLICT (repo_id) DO NOTHING - RETURNING id -) -SELECT id FROM ins -UNION ALL -SELECT id FROM github_repository WHERE repo_id = $2 AND NOT EXISTS (SELECT 1 FROM ins) -` - -type SaveGithubRepositoryParams struct { - ID uuid.UUID - RepoID int64 - Owner string - RepoName string -} - -func (q *Queries) SaveGithubRepository(ctx context.Context, arg SaveGithubRepositoryParams) (uuid.UUID, error) { - row := q.db.QueryRowContext(ctx, saveGithubRepository, - arg.ID, - arg.RepoID, - arg.Owner, - arg.RepoName, - ) - var id uuid.UUID - err := row.Scan(&id) - return id, err -} - -const saveMetaGithubRepository = `-- name: SaveMetaGithubRepository :exec -INSERT INTO meta_github_repository ( - id, - scan_id, - repository_id, - branch, - is_default_branch, - commit_id, - base_commit_id, - pull_request_id -) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8 -) -` - -type SaveMetaGithubRepositoryParams struct { - ID uuid.UUID - ScanID uuid.UUID - RepositoryID uuid.UUID - Branch sql.NullString - IsDefaultBranch sql.NullBool - CommitID string - BaseCommitID sql.NullString - PullRequestID sql.NullInt32 -} - -func (q *Queries) SaveMetaGithubRepository(ctx context.Context, arg SaveMetaGithubRepositoryParams) error { - _, err := q.db.ExecContext(ctx, saveMetaGithubRepository, - arg.ID, - arg.ScanID, - arg.RepositoryID, - arg.Branch, - arg.IsDefaultBranch, - arg.CommitID, - arg.BaseCommitID, - arg.PullRequestID, - ) - return err -} - -const savePackage = `-- name: SavePackage :exec -INSERT INTO packages ( - id, - target_type, - name, - version -) VALUES ( - $1, $2, $3, $4 -) -` - -type SavePackageParams struct { - ID string - TargetType string - Name string - Version string -} - -func (q *Queries) SavePackage(ctx context.Context, arg SavePackageParams) error { - _, err := q.db.ExecContext(ctx, savePackage, - arg.ID, - arg.TargetType, - arg.Name, - arg.Version, - ) - return err -} - -const saveResult = `-- name: SaveResult :exec -INSERT INTO results ( - id, - scan_id, - target, - target_type, - class -) VALUES ( - $1, $2, $3, $4, $5 -) -` - -type SaveResultParams struct { - ID uuid.UUID - ScanID uuid.UUID - Target string - TargetType string - Class TargetClass -} - -func (q *Queries) SaveResult(ctx context.Context, arg SaveResultParams) error { - _, err := q.db.ExecContext(ctx, saveResult, - arg.ID, - arg.ScanID, - arg.Target, - arg.TargetType, - arg.Class, - ) - return err -} - -const saveScan = `-- name: SaveScan :exec -INSERT INTO scans ( - id, - artifact_name, - artifact_type -) VALUES ( - $1, $2, $3 -) -` - -type SaveScanParams struct { - ID uuid.UUID - ArtifactName string - ArtifactType string -} - -func (q *Queries) SaveScan(ctx context.Context, arg SaveScanParams) error { - _, err := q.db.ExecContext(ctx, saveScan, arg.ID, arg.ArtifactName, arg.ArtifactType) - return err -} - -const saveVulnerability = `-- name: SaveVulnerability :exec -INSERT INTO vulnerabilities ( - id, - title, - severity, - published_at, - last_modified_at, - data -) VALUES ( - $1, $2, $3, $4, $5, $6 -) ON CONFLICT (id) -DO UPDATE SET - title = $2, - severity = $3, - published_at = $4, - last_modified_at = $5, - data = $6 -WHERE vulnerabilities.last_modified_at < $5 -` - -type SaveVulnerabilityParams struct { - ID string - Title string - Severity string - PublishedAt sql.NullTime - LastModifiedAt sql.NullTime - Data pqtype.NullRawMessage -} - -func (q *Queries) SaveVulnerability(ctx context.Context, arg SaveVulnerabilityParams) error { - _, err := q.db.ExecContext(ctx, saveVulnerability, - arg.ID, - arg.Title, - arg.Severity, - arg.PublishedAt, - arg.LastModifiedAt, - arg.Data, - ) - return err -} diff --git a/pkg/infra/gh/client.go b/pkg/infra/gh/client.go index 354c5c5d..b6e76c64 100644 --- a/pkg/infra/gh/client.go +++ b/pkg/infra/gh/client.go @@ -1,6 +1,10 @@ package gh import ( + "bytes" + "context" + _ "embed" + "encoding/json" "io" "log/slog" "net/http" @@ -9,30 +13,20 @@ import ( "github.com/bradleyfalzon/ghinstallation/v2" "github.com/google/go-github/v53/github" "github.com/m-mizutani/goerr" + "github.com/m-mizutani/octovy/pkg/domain/interfaces" "github.com/m-mizutani/octovy/pkg/domain/model" "github.com/m-mizutani/octovy/pkg/domain/types" + "github.com/m-mizutani/octovy/pkg/utils" ) -type Client interface { - GetArchiveURL(ctx *model.Context, input *GetArchiveURLInput) (*url.URL, error) - // CreateIssueComment(repo *model.GitHubRepo, prID int, body string) error - // CreateCheckRun(repo *model.GitHubRepo, commit string) (int64, error) - // UpdateCheckRun(repo *model.GitHubRepo, checkID int64, opt *github.UpdateCheckRunOptions) error -} - -type GetArchiveURLInput struct { - Owner string - Repo string - CommitID string - InstallID types.GitHubAppInstallID -} - -type clientImpl struct { +type Client struct { appID types.GitHubAppID pem types.GitHubAppPrivateKey } -func New(appID types.GitHubAppID, pem types.GitHubAppPrivateKey) (Client, error) { +var _ interfaces.GitHub = (*Client)(nil) + +func New(appID types.GitHubAppID, pem types.GitHubAppPrivateKey) (*Client, error) { if appID == 0 { return nil, goerr.Wrap(types.ErrInvalidOption, "appID is empty") } @@ -40,25 +34,34 @@ func New(appID types.GitHubAppID, pem types.GitHubAppPrivateKey) (Client, error) return nil, goerr.Wrap(types.ErrInvalidOption, "pem is empty") } - return &clientImpl{ + return &Client{ appID: appID, pem: pem, }, nil } -func (x *clientImpl) buildGithubClient(installID types.GitHubAppInstallID) (*github.Client, error) { +func (x *Client) buildGithubClient(installID types.GitHubAppInstallID) (*github.Client, error) { + httpClient, err := x.buildGithubHTTPClient(installID) + if err != nil { + return nil, err + } + return github.NewClient(httpClient), nil +} + +func (x *Client) buildGithubHTTPClient(installID types.GitHubAppInstallID) (*http.Client, error) { tr := http.DefaultTransport itr, err := ghinstallation.New(tr, int64(x.appID), int64(installID), []byte(x.pem)) if err != nil { - return nil, goerr.Wrap(err) + return nil, goerr.Wrap(err, "Failed to create github client") } - return github.NewClient(&http.Client{Transport: itr}), nil + client := &http.Client{Transport: itr} + return client, nil } -func (x *clientImpl) GetArchiveURL(ctx *model.Context, input *GetArchiveURLInput) (*url.URL, error) { - ctx.Logger().Info("Sending GetArchiveLink request", +func (x *Client) GetArchiveURL(ctx context.Context, input *interfaces.GetArchiveURLInput) (*url.URL, error) { + utils.CtxLogger(ctx).Info("Sending GetArchiveLink request", slog.Any("appID", x.appID), slog.Any("privateKey", x.pem), slog.Any("input", input), @@ -84,12 +87,12 @@ func (x *clientImpl) GetArchiveURL(ctx *model.Context, input *GetArchiveURLInput return nil, goerr.Wrap(err, "Failed to get archive link").With("status", r.StatusCode).With("body", string(body)) } - ctx.Logger().Debug("GetArchiveLink response", slog.Any("url", url), slog.Any("r", r)) + utils.CtxLogger(ctx).Debug("GetArchiveLink response", slog.Any("url", url), slog.Any("r", r)) return url, nil } -func (x *clientImpl) CreateIssue(ctx *model.Context, id types.GitHubAppInstallID, repo *model.GitHubRepo, req *github.IssueRequest) (*github.Issue, error) { +func (x *Client) CreateIssue(ctx context.Context, id types.GitHubAppInstallID, repo *model.GitHubRepo, req *github.IssueRequest) (*github.Issue, error) { client, err := x.buildGithubClient(id) if err != nil { return nil, err @@ -106,70 +109,207 @@ func (x *clientImpl) CreateIssue(ctx *model.Context, id types.GitHubAppInstallID return issue, nil } -/* -func (x *clientImpl) CreateIssueComment(repo *model.GitHubRepo, prID int, body string) error { - client, err := x.githubClient() +func (x *Client) CreateIssueComment(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, prID int, body string) error { + client, err := x.buildGithubClient(id) if err != nil { return err } - ctx := context.Background() comment := &github.IssueComment{Body: &body} - ret, resp, err := client.Issues.CreateComment(ctx, repo.Owner, repo.Name, prID, comment) + ret, resp, err := client.Issues.CreateComment(ctx, repo.Owner, repo.RepoName, prID, comment) if err != nil { return goerr.Wrap(err, "Failed to create github comment").With("repo", repo).With("prID", prID).With("comment", comment) } if resp.StatusCode != http.StatusCreated { return goerr.Wrap(err, "Failed to ") } - utils.Logger.With("comment", ret).Debug("Commented to PR") + utils.Logger().Debug("Commented to PR", "comment", ret) + + return nil +} + +//go:embed queries/list_comments.graphql +var queryListIssueComments string + +func (x *Client) ListIssueComments(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, prID int) ([]*model.GitHubIssueComment, error) { + type response struct { + Repository struct { + PullRequest struct { + Comments struct { + Edges []struct { + Cursor string `json:"cursor"` + Node struct { + ID string `json:"id"` + Author struct { + Login string `json:"login"` + } `json:"author"` + Body string `json:"body"` + IsMinimized bool `json:"isMinimized"` + } `json:"node"` + } `json:"edges"` + } `json:"comments"` + Title string `json:"title"` + } `json:"pullRequest"` + } `json:"repository"` + } + + var comments []*model.GitHubIssueComment + + var cursor *string + for { + vars := map[string]any{ + "owner": repo.Owner, + "name": repo.RepoName, + "issueNumber": prID, + } + if cursor != nil { + vars["cursor"] = *cursor + } + resp, err := x.queryGraphQL(ctx, id, &gqlRequest{ + Query: queryListIssueComments, + Variables: vars, + }) + + if err != nil { + return nil, err + } + + var data response + if err := json.Unmarshal(resp.Data, &data); err != nil { + return nil, goerr.Wrap(err, "Failed to unmarshal response") + } + + if len(data.Repository.PullRequest.Comments.Edges) == 0 { + break + } + + for _, edge := range data.Repository.PullRequest.Comments.Edges { + comments = append(comments, &model.GitHubIssueComment{ + ID: edge.Node.ID, + Login: edge.Node.Author.Login, + Body: edge.Node.Body, + IsMinimized: edge.Node.IsMinimized, + }) + cursor = &edge.Cursor + } + } + + return comments, nil +} + +//go:embed queries/minimize_comment.graphql +var queryMinimizeComment string + +func (x *Client) MinimizeComment(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, subjectID string) error { + req := &gqlRequest{ + Query: queryMinimizeComment, + Variables: map[string]any{ + "id": subjectID, + }, + } + + _, err := x.queryGraphQL(ctx, id, req) + if err != nil { + return err + } return nil } -func (x *clientImpl) CreateCheckRun(repo *model.GitHubRepo, commit string) (int64, error) { - client, err := x.githubClient() +type gqlRequest struct { + Query string `json:"query"` + Variables map[string]any `json:"variables"` +} +type gqlResponse struct { + Data json.RawMessage `json:"data"` + Error json.RawMessage `json:"errors"` +} + +func (x *Client) queryGraphQL(ctx context.Context, id types.GitHubAppInstallID, req *gqlRequest) (*gqlResponse, error) { + client, err := x.buildGithubHTTPClient(id) + if err != nil { + return nil, err + } + + rawReq, err := json.Marshal(req) + if err != nil { + return nil, goerr.Wrap(err, "Failed to marshal graphQL request").With("req", req) + } + + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, apiGraphQLEndpoint, bytes.NewReader(rawReq)) + if err != nil { + return nil, goerr.Wrap(err, "Failed to create graphQL request").With("req", req) + } + + resp, err := client.Do(httpReq) + if err != nil { + return nil, goerr.Wrap(err, "Failed to send graphQL request").With("req", req) + } + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, goerr.Wrap(err, "Failed to get graphQL response").With("req", httpReq).With("resp", resp).With("body", string(body)) + } + + var gqlResp gqlResponse + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, goerr.Wrap(err, "Failed to read response body").With("resp", resp) + } + if err := json.Unmarshal(body, &gqlResp); err != nil { + return nil, goerr.Wrap(err, "Failed to decode response").With("resp", resp) + } + + return &gqlResp, nil +} + +const ( + apiGraphQLEndpoint = "https://api.github.com/graphql" +) + +func (x *Client) CreateCheckRun(ctx context.Context, id types.GitHubAppInstallID, repo *model.GitHubRepo, commit string) (int64, error) { + client, err := x.buildGithubClient(id) if err != nil { return 0, err } - ctx := context.Background() opt := github.CreateCheckRunOptions{ Name: "Octovy: package vulnerability check", HeadSHA: commit, Status: github.String("in_progress"), } - run, resp, err := client.Checks.CreateCheckRun(ctx, repo.Owner, repo.Name, opt) + run, resp, err := client.Checks.CreateCheckRun(ctx, repo.Owner, repo.RepoName, opt) if err != nil { return 0, goerr.Wrap(err, "Failed to create check run").With("repo", repo).With("commit", commit) } if resp.StatusCode != http.StatusCreated { return 0, goerr.Wrap(err, "Failed to ") } - utils.Logger.With("run", run).Debug("Created check run") + utils.CtxLogger(ctx).With("run", run).Debug("Created check run") return *run.ID, nil } -func (x *clientImpl) UpdateCheckRun(repo *model.GitHubRepo, checkID int64, opt *github.UpdateCheckRunOptions) error { - client, err := x.githubClient() +func (x *Client) UpdateCheckRun(ctx context.Context, id types.GitHubAppInstallID, repo *model.GitHubRepo, checkID int64, opt *github.UpdateCheckRunOptions) error { + client, err := x.buildGithubClient(id) if err != nil { return err } - ctx := context.Background() - - _, resp, err := client.Checks.UpdateCheckRun(ctx, repo.Owner, repo.Name, checkID, *opt) + _, resp, err := client.Checks.UpdateCheckRun(ctx, repo.Owner, repo.RepoName, checkID, *opt) if err != nil { return goerr.Wrap(err, "Failed to update check status to complete").With("repo", repo).With("id", checkID).With("opt", opt) } if resp.StatusCode != http.StatusOK { return goerr.Wrap(err, "Failed to update status to complete") } - utils.Logger.With("repo", repo).With("id", checkID).Debug("Created check run") + utils.CtxLogger(ctx).Debug("Updated check run", + slog.Any("opt", opt), + slog.Any("resp", resp), + slog.Any("repo", repo), + slog.Any("checkID", checkID), + ) return nil } -*/ diff --git a/pkg/infra/gh/client_test.go b/pkg/infra/gh/client_test.go new file mode 100644 index 00000000..db999974 --- /dev/null +++ b/pkg/infra/gh/client_test.go @@ -0,0 +1,96 @@ +package gh_test + +import ( + "context" + "strconv" + "testing" + + "github.com/google/uuid" + "github.com/m-mizutani/gt" + "github.com/m-mizutani/octovy/pkg/domain/model" + "github.com/m-mizutani/octovy/pkg/domain/types" + "github.com/m-mizutani/octovy/pkg/infra/gh" + "github.com/m-mizutani/octovy/pkg/utils" +) + +func TestGitHubComment(t *testing.T) { + ghApp, installID := buildGitHubApp(t) + + ctx := context.Background() + repo := &model.GitHubRepo{ + Owner: "m-mizutani", + RepoName: "octovy-test-code", + } + ghApp.CreateIssueComment(ctx, repo, types.GitHubAppInstallID(installID), 1, "Hello, world") + + comments := gt.R1(ghApp.ListIssueComments(ctx, repo, types.GitHubAppInstallID(installID), 1)).NoError(t) + + utils.DumpJson(t, "comments.json", comments) +} + +func buildGitHubApp(t *testing.T) (*gh.Client, types.GitHubAppInstallID) { + var ( + strAppID = utils.LoadEnv(t, "TEST_GITHUB_APP_ID") + strInstallationID = utils.LoadEnv(t, "TEST_GITHUB_INSTALLATION_ID") + privateKey = utils.LoadEnv(t, "TEST_GITHUB_PRIVATE_KEY") + ) + + appID := gt.R1(strconv.ParseInt(strAppID, 10, 64)).NoError(t) + installID := gt.R1(strconv.ParseInt(strInstallationID, 10, 64)).NoError(t) + + ghApp := gt.R1(gh.New(types.GitHubAppID(appID), types.GitHubAppPrivateKey(privateKey))).NoError(t) + + return ghApp, types.GitHubAppInstallID(installID) +} + +func TestListComments(t *testing.T) { + ghApp, installID := buildGitHubApp(t) + + ctx := context.Background() + repo := &model.GitHubRepo{ + Owner: "m-mizutani", + RepoName: "octovy-test-code", + } + + comments, err := ghApp.ListIssueComments(ctx, repo, types.GitHubAppInstallID(installID), 2) + gt.NoError(t, err) + gt.A(t, comments).Longer(1).At(0, func(t testing.TB, v *model.GitHubIssueComment) { + gt.Equal(t, v.Body, "testing") + }) +} + +func TestHideComment(t *testing.T) { + ghApp, installID := buildGitHubApp(t) + + ctx := context.Background() + repo := &model.GitHubRepo{ + Owner: "m-mizutani", + RepoName: "octovy-test-code", + } + testIssueID := 2 + + slag := "comment-test:" + uuid.NewString() + gt.NoError(t, ghApp.CreateIssueComment(ctx, repo, installID, 2, slag)) + + comments, err := ghApp.ListIssueComments(ctx, repo, types.GitHubAppInstallID(installID), testIssueID) + gt.NoError(t, err) + + var subjectID string + for _, c := range comments { + if c.Body == slag { + gt.False(t, c.IsMinimized) + subjectID = c.ID + break + } + } + + gt.NotEqual(t, subjectID, "") + + gt.NoError(t, ghApp.MinimizeComment(ctx, repo, installID, subjectID)) + + comments, err = ghApp.ListIssueComments(ctx, repo, types.GitHubAppInstallID(installID), testIssueID) + gt.NoError(t, err) + gt.A(t, comments).Longer(1).Any(func(v *model.GitHubIssueComment) bool { + return v.ID == subjectID && v.IsMinimized + }) +} diff --git a/pkg/infra/gh/comments.json b/pkg/infra/gh/comments.json new file mode 100644 index 00000000..19765bd5 --- /dev/null +++ b/pkg/infra/gh/comments.json @@ -0,0 +1 @@ +null diff --git a/pkg/infra/gh/queries/list_comments.graphql b/pkg/infra/gh/queries/list_comments.graphql new file mode 100644 index 00000000..13431ac8 --- /dev/null +++ b/pkg/infra/gh/queries/list_comments.graphql @@ -0,0 +1,20 @@ +query ($owner: String!, $name: String!, $issueNumber: Int!, $cursor: String) { + repository(owner: $owner, name: $name) { + pullRequest(number: $issueNumber) { + title + comments(first: 100, after: $cursor) { + edges { + cursor + node { + id + author { + login + } + body + isMinimized + } + } + } + } + } +} diff --git a/pkg/infra/gh/queries/minimize_comment.graphql b/pkg/infra/gh/queries/minimize_comment.graphql new file mode 100644 index 00000000..bebd4fce --- /dev/null +++ b/pkg/infra/gh/queries/minimize_comment.graphql @@ -0,0 +1,8 @@ +mutation ($id: ID!) { + minimizeComment(input: { subjectId: $id, classifier: OUTDATED }) { + minimizedComment { + isMinimized + minimizedReason + } + } +} diff --git a/pkg/infra/trivy/client.go b/pkg/infra/trivy/client.go index 23006839..2757e22a 100644 --- a/pkg/infra/trivy/client.go +++ b/pkg/infra/trivy/client.go @@ -2,14 +2,15 @@ package trivy import ( "bytes" + "context" "os/exec" "github.com/m-mizutani/goerr" - "github.com/m-mizutani/octovy/pkg/domain/model" + "github.com/m-mizutani/octovy/pkg/utils" ) type Client interface { - Run(ctx *model.Context, args []string) error + Run(ctx context.Context, args []string) error } type clientImpl struct { @@ -22,7 +23,10 @@ func New(path string) Client { } } -func (x *clientImpl) Run(ctx *model.Context, args []string) error { +func (x *clientImpl) Run(ctx context.Context, args []string) error { + // Why: The arguments are not from user input + // nosemgrep: go.lang.security.audit.dangerous-exec-command.dangerous-exec-command + // #nosec: G204 cmd := exec.CommandContext(ctx, x.path, args...) var stdout bytes.Buffer var stderr bytes.Buffer @@ -30,7 +34,7 @@ func (x *clientImpl) Run(ctx *model.Context, args []string) error { cmd.Stderr = &stderr if err := cmd.Run(); err != nil { - ctx.Logger().With("stderr", stderr.String()).With("stdout", stdout.String()).Error("trivy failed") + utils.CtxLogger(ctx).With("stderr", stderr.String()).With("stdout", stdout.String()).Error("trivy failed") return goerr.Wrap(err, "executing trivy"). With("stderr", stderr.String()). With("stdout", stdout.String()) diff --git a/pkg/infra/trivy/client_test.go b/pkg/infra/trivy/client_test.go index 8fb0d586..b5b6fc6f 100644 --- a/pkg/infra/trivy/client_test.go +++ b/pkg/infra/trivy/client_test.go @@ -1,17 +1,16 @@ package trivy_test import ( + "context" "encoding/json" "os" "path/filepath" "testing" "github.com/m-mizutani/gt" - "github.com/m-mizutani/octovy/pkg/domain/model" "github.com/m-mizutani/octovy/pkg/infra/trivy" - ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" - ttypes "github.com/aquasecurity/trivy/pkg/types" + trivy_model "github.com/m-mizutani/octovy/pkg/domain/model/trivy" ) func Test(t *testing.T) { @@ -27,7 +26,7 @@ func Test(t *testing.T) { gt.NoError(t, tmp.Close()) client := trivy.New(path) - ctx := model.NewContext() + ctx := context.Background() gt.NoError(t, client.Run(ctx, []string{ "fs", target, @@ -36,13 +35,17 @@ func Test(t *testing.T) { "--list-all-pkgs", })) - var report ttypes.Report + var report trivy_model.Report body := gt.R1(os.ReadFile(tmp.Name())).NoError(t) gt.NoError(t, json.Unmarshal(body, &report)) gt.V(t, report.SchemaVersion).Equal(2) - gt.A(t, report.Results).Length(1).At(0, func(t testing.TB, v ttypes.Result) { - gt.A(t, v.Packages).Any(func(v ftypes.Package) bool { - return v.Name == "github.com/m-mizutani/goerr" - }) + gt.A(t, report.Results).Longer(0).Any(func(v trivy_model.Result) bool { + if v.Target == "go.mod" { + gt.A(t, v.Packages).Any(func(v trivy_model.Package) bool { + return v.Name == "github.com/m-mizutani/goerr" + }) + } + + return v.Target == "go.mod" }) } diff --git a/pkg/usecase/comment_githug_pr.go b/pkg/usecase/comment_githug_pr.go new file mode 100644 index 00000000..0171edf1 --- /dev/null +++ b/pkg/usecase/comment_githug_pr.go @@ -0,0 +1,142 @@ +package usecase + +import ( + "bytes" + "context" + _ "embed" + "encoding/json" + "strings" + "text/template" + + "github.com/m-mizutani/goerr" + "github.com/m-mizutani/octovy/pkg/domain/logic" + "github.com/m-mizutani/octovy/pkg/domain/model" + "github.com/m-mizutani/octovy/pkg/domain/model/trivy" + "github.com/m-mizutani/octovy/pkg/domain/types" +) + +func (x *UseCase) CommentGitHubPR(ctx context.Context, input *model.ScanGitHubRepoInput, report *trivy.Report) error { + if err := input.Validate(); err != nil { + return err + } + + if nil == input.GitHubMetadata.PullRequest { + return goerr.New("PullRequest is not set") + } + + if x.clients.GitHubApp() == nil { + return goerr.New("GitHubApp client is not set") + } + if x.clients.Storage() == nil { + return goerr.New("Storage client is not configured") + } + + var added, fixed trivy.Results + target := model.GitHubMetadata{ + GitHubCommit: model.GitHubCommit{ + GitHubRepo: input.GitHubMetadata.GitHubRepo, + CommitID: input.GitHubMetadata.PullRequest.BaseCommitID, + }, + } + + commitKey := toStorageCommitKey(target) + r, err := x.clients.Storage().Get(ctx, commitKey) + if err != nil { + return err + } else if r != nil { + defer r.Close() + + var oldScan model.Scan + if err := json.NewDecoder(r).Decode(&oldScan); err != nil { + return goerr.Wrap(err, "Failed to decode old scan result") + } + fixed, added = logic.DiffResults(&oldScan.Report, report) + } + + body, err := renderScanReport(report, added, fixed) + if err != nil { + return err + } + + if err := x.hideGitHubOldComments(ctx, input); err != nil { + return err + } + + if err := x.clients.GitHubApp().CreateIssueComment(ctx, &input.GitHubMetadata.GitHubRepo, input.InstallID, input.PullRequest.Number, body); err != nil { + return err + } + + return nil +} + +func (x *UseCase) hideGitHubOldComments(ctx context.Context, input *model.ScanGitHubRepoInput) error { + if nil == input.GitHubMetadata.PullRequest { + return goerr.New("PullRequest is not set") + } + + if x.clients.GitHubApp() == nil { + return goerr.New("GitHubApp client is not set") + } + + comments, err := x.clients.GitHubApp().ListIssueComments(ctx, &input.GitHubMetadata.GitHubRepo, input.InstallID, input.PullRequest.Number) + if err != nil { + return err + } + + for _, comment := range comments { + if !comment.IsMinimized && strings.HasPrefix(comment.Body, types.GitHubCommentSignature) { + if err := x.clients.GitHubApp().MinimizeComment(ctx, &input.GitHubMetadata.GitHubRepo, input.InstallID, comment.ID); err != nil { + return err + } + } + } + + return nil +} + +type scanReport struct { + Signature string + Metadata scanReportMetadata + Report *trivy.Report + Added trivy.Results + Fixed trivy.Results +} + +type scanReportMetadata struct { + TotalVulnCount int + FixableVulnCount int +} + +//go:embed templates/comment_body.md +var commentBodyTemplateData string + +var commentBodyTemplate *template.Template + +func init() { + commentBodyTemplate = template.Must(template.New("commentBody").Parse(commentBodyTemplateData)) +} + +func renderScanReport(report *trivy.Report, added, fixed trivy.Results) (string, error) { + data := scanReport{ + Signature: types.GitHubCommentSignature, + Report: report, + Added: added, + Fixed: fixed, + } + + for _, result := range report.Results { + for _, vuln := range result.Vulnerabilities { + data.Metadata.TotalVulnCount++ + if vuln.FixedVersion != "" { + data.Metadata.FixableVulnCount++ + } + } + } + + var buf bytes.Buffer + if err := commentBodyTemplate.Execute(&buf, data); err != nil { + return "", goerr.Wrap(err, "failed to render comment body template") + } + + return buf.String(), nil +} diff --git a/pkg/usecase/comment_githug_pr_test.go b/pkg/usecase/comment_githug_pr_test.go new file mode 100644 index 00000000..7d66479d --- /dev/null +++ b/pkg/usecase/comment_githug_pr_test.go @@ -0,0 +1,135 @@ +package usecase_test + +import ( + "context" + "os" + "testing" + + "github.com/m-mizutani/gt" + "github.com/m-mizutani/octovy/pkg/domain/interfaces" + "github.com/m-mizutani/octovy/pkg/domain/model" + "github.com/m-mizutani/octovy/pkg/domain/model/trivy" + "github.com/m-mizutani/octovy/pkg/domain/types" + "github.com/m-mizutani/octovy/pkg/infra" + "github.com/m-mizutani/octovy/pkg/usecase" +) + +func TestRenderScanReport(t *testing.T) { + report := trivy.Report{ + Results: []trivy.Result{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0001", PkgName: "pkg1", Vulnerability: trivy.Vulnerability{Title: "Vuln title1", Severity: "HIGH"}}, + {VulnerabilityID: "CVE-0000-0002", PkgName: "pkg2", Vulnerability: trivy.Vulnerability{Title: "Vuln title2", Severity: "CRITICAL"}}, + }, + }, + { + Target: "target2", + Vulnerabilities: []trivy.DetectedVulnerability{ + {VulnerabilityID: "CVE-0000-0003", PkgName: "pkg4", Vulnerability: trivy.Vulnerability{Title: "Vuln title3", Severity: "CRITICAL"}}, + }, + }, + }, + } + added := trivy.Results{ + { + Target: "target1", + Vulnerabilities: []trivy.DetectedVulnerability{ + { + VulnerabilityID: "CVE-0000-0002", + PkgName: "pkg2", + Vulnerability: trivy.Vulnerability{ + Title: "Vuln title2", + Description: "Vuln description2", + Severity: "CRITICAL", + References: []string{ + "https://example.com", + "https://example.com/2", + }, + }, + }, + }, + }, + } + fixed := trivy.Results{ + { + Target: "target2", + Vulnerabilities: []trivy.DetectedVulnerability{ + { + VulnerabilityID: "CVE-0000-0003", + PkgName: "pkg3", + Vulnerability: trivy.Vulnerability{ + Title: "Vuln title3", + }, + }, + }, + }, + } + + body, err := usecase.RenderScanReport(&report, added, fixed) + gt.NoError(t, err) + gt.NoError(t, os.WriteFile("templates/test_comment_body.md", []byte(body), 0644)) +} + +func TestHideGitHubOldComments(t *testing.T) { + mockGH := &interfaces.GitHubMock{} + + uc := usecase.New(infra.New( + infra.WithGitHubApp(mockGH), + )) + + type testCase struct { + comments []*model.GitHubIssueComment + subjectIDs []string + } + + runTest := func(tc testCase) func(t *testing.T) { + return func(t *testing.T) { + mockGH.MockListIssueComments = func(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, prID int) ([]*model.GitHubIssueComment, error) { + return tc.comments, nil + } + + var minimized []string + mockGH.MockMinimizeComment = func(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, subjectID string) error { + minimized = append(minimized, subjectID) + return nil + } + + input := &model.ScanGitHubRepoInput{ + GitHubMetadata: model.GitHubMetadata{ + GitHubCommit: model.GitHubCommit{ + GitHubRepo: model.GitHubRepo{ + Owner: "blue", + RepoName: "magic", + }, + }, + PullRequest: &model.GitHubPullRequest{Number: 123}, + }, + InstallID: 12345, + } + + ctx := context.Background() + gt.NoError(t, uc.HideGitHubOldComments(ctx, input)) + gt.V(t, minimized).Equal(tc.subjectIDs) + } + } + + t.Run("no comments", runTest(testCase{})) + + t.Run("no minimized comments without signature", runTest(testCase{ + comments: []*model.GitHubIssueComment{ + {ID: "abc", Body: "comment1", IsMinimized: false}, + {ID: "edf", Body: "comment2", IsMinimized: true}, + }, + subjectIDs: nil, + })) + + t.Run("minimize comments with signature", runTest(testCase{ + comments: []*model.GitHubIssueComment{ + {ID: "abc", Body: types.GitHubCommentSignature + "\ncomment1", IsMinimized: false}, + {ID: "edf", Body: types.GitHubCommentSignature + "\ncomment2", IsMinimized: true}, + }, + subjectIDs: []string{"abc"}, + })) +} diff --git a/pkg/usecase/diff_github_repo.go b/pkg/usecase/diff_github_repo.go deleted file mode 100644 index c1a9cafb..00000000 --- a/pkg/usecase/diff_github_repo.go +++ /dev/null @@ -1,121 +0,0 @@ -package usecase - -import ( - "database/sql" - "encoding/json" - - ttypes "github.com/aquasecurity/trivy/pkg/types" - "github.com/m-mizutani/goerr" - "github.com/m-mizutani/octovy/pkg/domain/model" - "github.com/m-mizutani/octovy/pkg/infra/db" -) - -type DiffResultKey struct { - Class string `json:"class"` - Target string `json:"target"` - TargetType string `json:"type"` -} - -type DiffStatus string - -const ( - DiffStatusAdd = DiffStatus("add") - DiffStatusDel = DiffStatus("del") - DiffStatusMod = DiffStatus("mod") -) - -type DiffResult struct { - DiffResultKey - Status DiffStatus `json:"status"` - Add []ttypes.DetectedVulnerability `json:"add"` - Del []ttypes.DetectedVulnerability `json:"del"` -} - -func getVulnDiffForGitHubRepo(ctx *model.Context, dbClient *sql.DB, report *ttypes.Report, commit *model.GitHubCommit) ([]DiffResult, error) { - reportResults := map[DiffResultKey]*ttypes.Result{} - for i, result := range report.Results { - key := DiffResultKey{ - Class: string(result.Class), - Target: result.Target, - TargetType: string(result.Type), - } - reportResults[key] = &report.Results[i] - } - - query := db.New(dbClient) - resp, err := query.GetLatestResultsByCommit(ctx, db.GetLatestResultsByCommitParams{ - RepoID: commit.RepoID, - CommitID: commit.CommitID, - }) - if err != nil { - return nil, goerr.Wrap(err, "failed to get latest results by commit").With("commit", commit) - } - - var diffResults []DiffResult - for _, r := range resp { - oldVulns, err := query.GetVulnerabilitiesByResultID(ctx, r.ID) - if err != nil { - return nil, goerr.Wrap(err, "failed to get vulnerabilities by result ID").With("resultID", r.ID) - } - oldVulnMap := map[string]ttypes.DetectedVulnerability{} - for _, old := range oldVulns { - var v ttypes.DetectedVulnerability - if err := json.Unmarshal(old.Data.RawMessage, &v); err != nil { - return nil, goerr.Wrap(err, "failed to unmarshal vulnerability").With("resultID", r.ID).With("data", old.Data) - } - oldVulnMap[v.VulnerabilityID] = v - } - - key := DiffResultKey{ - Class: string(r.Class), - Target: r.Target, - TargetType: r.TargetType, - } - - result, ok := reportResults[key] - if !ok { - var vulnList []ttypes.DetectedVulnerability - for _, v := range oldVulnMap { - vulnList = append(vulnList, v) - } - diffResults = append(diffResults, DiffResult{ - DiffResultKey: key, - Status: DiffStatusDel, - Del: vulnList, - }) - continue - } - - var add, del []ttypes.DetectedVulnerability - for _, v := range result.Vulnerabilities { - if _, ok := oldVulnMap[v.VulnerabilityID]; !ok { - add = append(add, v) - } - delete(oldVulnMap, v.VulnerabilityID) - } - for _, v := range oldVulnMap { - del = append(del, v) - } - - if len(add) > 0 || len(del) > 0 { - diffResults = append(diffResults, DiffResult{ - DiffResultKey: key, - Status: DiffStatusMod, - Add: add, - Del: del, - }) - } - - delete(reportResults, key) - } - - for key, result := range reportResults { - diffResults = append(diffResults, DiffResult{ - DiffResultKey: key, - Status: DiffStatusAdd, - Add: result.Vulnerabilities, - }) - } - - return diffResults, nil -} diff --git a/pkg/usecase/diff_github_repo_test.go b/pkg/usecase/diff_github_repo_test.go deleted file mode 100644 index 68b6542b..00000000 --- a/pkg/usecase/diff_github_repo_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package usecase_test - -import ( - "encoding/json" - "testing" - "time" - - ptypes "github.com/aquasecurity/trivy-db/pkg/types" - ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" - ttypes "github.com/aquasecurity/trivy/pkg/types" - "github.com/google/uuid" - "github.com/m-mizutani/gots/ptr" - "github.com/m-mizutani/gots/rands" - "github.com/m-mizutani/gt" - "github.com/m-mizutani/octovy/pkg/domain/model" - "github.com/m-mizutani/octovy/pkg/usecase" -) - -func deepCopy[T any](t *testing.T, src T) T { - var dst T - data := gt.R1(json.Marshal(src)).NoError(t) - gt.NoError(t, json.Unmarshal(data, &dst)) - return dst -} - -func TestGetVulnDiffForGitHubRepo(t *testing.T) { - dbClient := newTestDB(t) - ctx := model.NewContext() - meta := &usecase.GitHubRepoMetadata{ - GitHubCommit: model.GitHubCommit{ - GitHubRepo: model.GitHubRepo{ - RepoID: 287748079, - Owner: "m-mizutani", - RepoName: "octovy", - }, - CommitID: uuid.NewString(), - }, - } - - now := time.Now() - salt := rands.AlphaNum(10) - baseReport := ttypes.Report{ - ArtifactName: "github.com/m-mizutani/octovy", - ArtifactType: ttypes.ClassLangPkg, - Results: ttypes.Results{ - { - Target: "Gemfile.lock", - Class: ttypes.ClassLangPkg, - Type: "bundler", - Packages: []ftypes.Package{ - { - Name: "octokit_" + salt, - Version: "4.18.0", - }, - }, - Vulnerabilities: []ttypes.DetectedVulnerability{ - { - VulnerabilityID: "CVE-2020-1234-" + salt, - PkgName: "octokit_" + salt, - InstalledVersion: "4.18.0", - Vulnerability: ptypes.Vulnerability{ - Title: "CVE-2020-1234", - Description: "test", - Severity: "MIDDLE", - References: []string{"https://example.com"}, - PublishedDate: ptr.To(now), - LastModifiedDate: ptr.To(now), - }, - }, - }, - }, - }, - } - - gt.NoError(t, usecase.SaveScanReportGitHubRepo(ctx, dbClient, &baseReport, meta)) - - t.Run("add vuln", func(t *testing.T) { - report := deepCopy(t, baseReport) - report.Results[0].Vulnerabilities = append(report.Results[0].Vulnerabilities, ttypes.DetectedVulnerability{ - VulnerabilityID: "CVE-2020-5678-" + salt, - PkgName: "octokit_" + salt, - InstalledVersion: "4.18.0", - Vulnerability: ptypes.Vulnerability{ - Title: "CVE-2020-5678", - Description: "test", - Severity: "MIDDLE", - References: []string{"https://example.com"}, - PublishedDate: ptr.To(now), - LastModifiedDate: ptr.To(now), - }, - }) - - report.Results[0].Packages = append(report.Results[0].Packages, ftypes.Package{ - Name: "octokit_" + salt, - Version: "4.18.0", - }) - - diffResults, err := usecase.GetVulnDiffForGitHubRepo(ctx, dbClient, &report, &meta.GitHubCommit) - gt.NoError(t, err) - gt.A(t, diffResults).Must().Length(1).At(0, func(t testing.TB, v usecase.DiffResult) { - gt.Equal(t, v.Status, usecase.DiffStatusMod) - gt.A(t, v.Add).Must().Length(1).At(0, func(t testing.TB, v ttypes.DetectedVulnerability) { - gt.Equal(t, "CVE-2020-5678-"+salt, v.VulnerabilityID) - }) - gt.A(t, v.Del).Must().Length(0) - }) - }) - - t.Run("del vuln", func(t *testing.T) { - report := deepCopy(t, baseReport) - report.Results[0].Vulnerabilities = []ttypes.DetectedVulnerability{} - - diffResults, err := usecase.GetVulnDiffForGitHubRepo(ctx, dbClient, &report, &meta.GitHubCommit) - gt.NoError(t, err) - gt.A(t, diffResults).Must().Length(1).At(0, func(t testing.TB, v usecase.DiffResult) { - gt.Equal(t, v.Status, usecase.DiffStatusMod) - gt.A(t, v.Add).Must().Length(0) - gt.A(t, v.Del).Must().Length(1).At(0, func(t testing.TB, v ttypes.DetectedVulnerability) { - gt.Equal(t, "CVE-2020-1234-"+salt, v.VulnerabilityID) - }) - }) - }) - - t.Run("add result", func(t *testing.T) { - report := deepCopy(t, baseReport) - report.Results = append(report.Results, ttypes.Result{ - Target: "go.mod", - Class: ttypes.ClassLangPkg, - Type: "go", - Vulnerabilities: []ttypes.DetectedVulnerability{ - { - VulnerabilityID: "CVE-2020-3456-" + salt, - PkgName: "github.com/m-mizutani/octovy", - InstalledVersion: "v1.0.0", - Vulnerability: ptypes.Vulnerability{ - Title: "CVE-2020-3456", - Description: "test", - Severity: "MIDDLE", - }, - }, - }, - }) - - diffResults, err := usecase.GetVulnDiffForGitHubRepo(ctx, dbClient, &report, &meta.GitHubCommit) - gt.NoError(t, err) - gt.A(t, diffResults).Must().Length(1).At(0, func(t testing.TB, v usecase.DiffResult) { - gt.Equal(t, v.Status, usecase.DiffStatusAdd) - gt.A(t, v.Add).Must().Length(1).At(0, func(t testing.TB, v ttypes.DetectedVulnerability) { - gt.Equal(t, "CVE-2020-3456-"+salt, v.VulnerabilityID) - }) - gt.A(t, v.Del).Must().Length(0) - }) - }) - - t.Run("del result", func(t *testing.T) { - report := deepCopy(t, baseReport) - report.Results = []ttypes.Result{} - - diffResults, err := usecase.GetVulnDiffForGitHubRepo(ctx, dbClient, &report, &meta.GitHubCommit) - gt.NoError(t, err) - gt.A(t, diffResults).Must().Length(1).At(0, func(t testing.TB, v usecase.DiffResult) { - gt.Equal(t, v.Status, usecase.DiffStatusDel) - gt.A(t, v.Add).Must().Length(0) - gt.A(t, v.Del).Must().Length(1).At(0, func(t testing.TB, v ttypes.DetectedVulnerability) { - gt.Equal(t, "CVE-2020-1234-"+salt, v.VulnerabilityID) - }) - }) - }) -} diff --git a/pkg/usecase/export_test.go b/pkg/usecase/export_test.go index 73df01a1..d92ff4f9 100644 --- a/pkg/usecase/export_test.go +++ b/pkg/usecase/export_test.go @@ -1,7 +1,13 @@ package usecase -var ( - CalcPackageID = calcPackageID - SaveScanReportGitHubRepo = saveScanReportGitHubRepo - GetVulnDiffForGitHubRepo = getVulnDiffForGitHubRepo +import ( + "context" + + "github.com/m-mizutani/octovy/pkg/domain/model" ) + +var RenderScanReport = renderScanReport + +func (x *UseCase) HideGitHubOldComments(ctx context.Context, input *model.ScanGitHubRepoInput) error { + return x.hideGitHubOldComments(ctx, input) +} diff --git a/pkg/usecase/insert_scan_result.go b/pkg/usecase/insert_scan_result.go new file mode 100644 index 00000000..a2e21fa8 --- /dev/null +++ b/pkg/usecase/insert_scan_result.go @@ -0,0 +1,121 @@ +package usecase + +import ( + "context" + "encoding/json" + "io" + "strings" + "time" + + "cloud.google.com/go/bigquery" + "github.com/m-mizutani/bqs" + "github.com/m-mizutani/goerr" + "github.com/m-mizutani/octovy/pkg/domain/interfaces" + "github.com/m-mizutani/octovy/pkg/domain/model" + "github.com/m-mizutani/octovy/pkg/domain/model/trivy" + "github.com/m-mizutani/octovy/pkg/domain/types" +) + +func (x *UseCase) InsertScanResult(ctx context.Context, meta model.GitHubMetadata, report trivy.Report) error { + if err := report.Validate(); err != nil { + return goerr.Wrap(err, "invalid trivy report") + } + + scan := &model.Scan{ + ID: types.NewScanID(), + Timestamp: time.Now().UTC(), + GitHub: meta, + Report: report, + } + + if x.clients.BigQuery() != nil { + schema, err := createOrUpdateBigQueryTable(ctx, x.clients.BigQuery(), x.tableID, scan) + if err != nil { + return err + } + + rawRecord := &model.ScanRawRecord{ + Scan: *scan, + Timestamp: scan.Timestamp.UnixMicro(), + } + if err := x.clients.BigQuery().Insert(ctx, x.tableID, schema, rawRecord); err != nil { + return goerr.Wrap(err, "failed to insert scan data to BigQuery") + } + } + + if x.clients.BigQuery() != nil { + raw, err := json.Marshal(scan) + if err != nil { + return goerr.Wrap(err, "failed to marshal scan data") + } + + commitKey := toStorageCommitKey(scan.GitHub) + branchKey := toStorageBranchKey(scan.GitHub) + + for _, key := range []string{commitKey, branchKey} { + buf := strings.NewReader(string(raw)) + reader := io.NopCloser(buf) + if err := x.clients.Storage().Put(ctx, key, reader); err != nil { + return err + } + } + } + return nil +} + +func toStorageCommitKey(meta model.GitHubMetadata) string { + return strings.Join([]string{ + meta.Owner, + meta.RepoName, + "commit", + meta.CommitID, + "scan.json.gz", + }, "/") +} + +func toStorageBranchKey(meta model.GitHubMetadata) string { + return strings.Join([]string{ + meta.Owner, + meta.RepoName, + "branch", + meta.Branch, + "scan.json.gz", + }, "/") +} + +func createOrUpdateBigQueryTable(ctx context.Context, bq interfaces.BigQuery, tableID types.BQTableID, scan *model.Scan) (bigquery.Schema, error) { + schema, err := bqs.Infer(scan) + if err != nil { + return nil, goerr.Wrap(err, "failed to infer scan schema") + } + + metaData, err := bq.GetMetadata(ctx, tableID) + if err != nil { + return nil, goerr.Wrap(err, "failed to create BigQuery table") + } + if metaData == nil { + if err := bq.CreateTable(ctx, tableID, &bigquery.TableMetadata{ + Schema: schema, + }); err != nil { + return nil, goerr.Wrap(err, "failed to create BigQuery table") + } + + return schema, nil + } + + if bqs.Equal(metaData.Schema, schema) { + return schema, nil + } + + mergedSchema, err := bqs.Merge(metaData.Schema, schema) + if err != nil { + return nil, goerr.Wrap(err, "failed to merge BigQuery schema") + } + if err := bq.UpdateTable(ctx, tableID, bigquery.TableMetadataToUpdate{ + Schema: mergedSchema, + }, metaData.ETag); err != nil { + return nil, goerr.Wrap(err, "failed to update BigQuery table") + } + + return mergedSchema, nil +} diff --git a/pkg/usecase/mock.go b/pkg/usecase/mock.go index 818ca84a..9f1d1233 100644 --- a/pkg/usecase/mock.go +++ b/pkg/usecase/mock.go @@ -1,15 +1,29 @@ package usecase -import "github.com/m-mizutani/octovy/pkg/domain/model" +import ( + "context" + + "github.com/m-mizutani/octovy/pkg/domain/interfaces" + "github.com/m-mizutani/octovy/pkg/domain/model" + "github.com/m-mizutani/octovy/pkg/domain/model/trivy" +) type Mock struct { - MockScanGitHubRepo func(ctx *model.Context, input *ScanGitHubRepoInput) error + MockInsertScanResult func(ctx context.Context, meta model.GitHubMetadata, report trivy.Report) error + MockScanGitHubRepo func(ctx context.Context, input *model.ScanGitHubRepoInput) error } -func NewMock() UseCase { +func NewMock() *Mock { return &Mock{} } -func (x *Mock) ScanGitHubRepo(ctx *model.Context, input *ScanGitHubRepoInput) error { +var _ interfaces.UseCase = &Mock{} + +func (x *Mock) InsertScanResult(ctx context.Context, meta model.GitHubMetadata, report trivy.Report) error { + return x.MockInsertScanResult(ctx, meta, report) +} + +// ScanGitHubRepo implements interfaces.UseCase. +func (x *Mock) ScanGitHubRepo(ctx context.Context, input *model.ScanGitHubRepoInput) error { return x.MockScanGitHubRepo(ctx, input) } diff --git a/pkg/usecase/mock_test.go b/pkg/usecase/mock_test.go new file mode 100644 index 00000000..f85cc59a --- /dev/null +++ b/pkg/usecase/mock_test.go @@ -0,0 +1 @@ +package usecase_test diff --git a/pkg/usecase/save_scan_report.go b/pkg/usecase/save_scan_report.go deleted file mode 100644 index 9cbb019a..00000000 --- a/pkg/usecase/save_scan_report.go +++ /dev/null @@ -1,245 +0,0 @@ -package usecase - -import ( - "bytes" - "crypto/sha256" - "database/sql" - "encoding/hex" - "encoding/json" - - "github.com/google/uuid" - "github.com/m-mizutani/goerr" - "github.com/m-mizutani/octovy/pkg/domain/model" - "github.com/m-mizutani/octovy/pkg/infra/db" - "github.com/m-mizutani/octovy/pkg/utils" - "github.com/sqlc-dev/pqtype" - - ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" - ttypes "github.com/aquasecurity/trivy/pkg/types" -) - -func saveScanReportGitHubRepo(ctx *model.Context, dbClient *sql.DB, report *ttypes.Report, meta *GitHubRepoMetadata) error { - for _, result := range report.Results { - if err := saveVulnerabilities(ctx, dbClient, result.Vulnerabilities); err != nil { - return err - } - - if err := savePackages(ctx, dbClient, result.Type, result.Packages); err != nil { - return err - } - } - - if err := saveScanGitHubRepo(ctx, dbClient, report, meta); err != nil { - return err - } - - return nil -} - -func saveScanGitHubRepo(ctx *model.Context, dbClient *sql.DB, report *ttypes.Report, meta *GitHubRepoMetadata) error { - tx, err := dbClient.Begin() - if err != nil { - return goerr.Wrap(err, "starting transaction") - } - defer utils.SafeRollback(tx) - q := db.New(tx) - - scanID := uuid.New() - if err := q.SaveScan(ctx, db.SaveScanParams{ - ID: scanID, - ArtifactName: report.ArtifactName, - ArtifactType: string(report.ArtifactType), - }); err != nil { - return goerr.Wrap(err, "saving scan") - } - - repoID, err := q.SaveGithubRepository(ctx, db.SaveGithubRepositoryParams{ - ID: uuid.New(), - RepoID: meta.RepoID, - Owner: meta.Owner, - RepoName: meta.RepoName, - }) - if err != nil { - return goerr.Wrap(err, "saving github repository") - } - - if err := q.SaveMetaGithubRepository(ctx, db.SaveMetaGithubRepositoryParams{ - ID: uuid.New(), - ScanID: scanID, - RepositoryID: repoID, - CommitID: meta.CommitID, - Branch: sql.NullString{ - String: meta.Branch, - Valid: meta.Branch != "", - }, - IsDefaultBranch: sql.NullBool{ - Bool: meta.IsDefaultBranch, - Valid: meta.Branch != "", - }, - BaseCommitID: sql.NullString{ - String: meta.BaseCommitID, - Valid: meta.BaseCommitID != "", - }, - PullRequestID: sql.NullInt32{ - Int32: int32(meta.PullRequestID), - Valid: meta.PullRequestID != 0, - }, - }); err != nil { - return goerr.Wrap(err, "saving meta github repository") - } - - for _, result := range report.Results { - // @TODO: Support other types - if result.Class != ttypes.ClassLangPkg && result.Class != ttypes.ClassOSPkg { - continue - } - - resultID := uuid.New() - if err := q.SaveResult(ctx, db.SaveResultParams{ - ID: resultID, - ScanID: scanID, - Target: result.Target, - TargetType: string(result.Type), - Class: db.TargetClass(result.Class), - }); err != nil { - return goerr.Wrap(err, "saving result") - } - - for _, vuln := range result.Vulnerabilities { - data, err := json.Marshal(vuln) - if err != nil { - return goerr.Wrap(err, "marshaling vulnerability").With("vuln", vuln) - } - - if err := q.SaveDetectedVulnerability(ctx, db.SaveDetectedVulnerabilityParams{ - ID: uuid.New(), - ResultID: resultID, - VulnID: vuln.VulnerabilityID, - PkgID: calcPackageID(result.Type, vuln.PkgName, vuln.InstalledVersion), - InstalledVersion: sql.NullString{ - String: vuln.InstalledVersion, - Valid: vuln.InstalledVersion != "", - }, - FixedVersion: sql.NullString{ - String: vuln.FixedVersion, - Valid: vuln.FixedVersion != "", - }, - Data: pqtype.NullRawMessage{Valid: true, RawMessage: data}, - }); err != nil { - return goerr.Wrap(err, "saving result vulnerability") - } - } - - for _, pkg := range result.Packages { - if err := q.SaveDetectedPackage(ctx, db.SaveDetectedPackageParams{ - ID: uuid.New(), - ResultID: resultID, - PkgID: calcPackageID(result.Type, pkg.Name, pkg.Version), - }); err != nil { - return goerr.Wrap(err, "saving result package") - } - } - } - - if err := tx.Commit(); err != nil { - return goerr.Wrap(err, "commit transaction") - } - - return nil -} - -// calcPackageID returns a unique ID for a package. It is calculated by sha512 from package name, version and type. -func calcPackageID(typ, name, version string) string { - src := bytes.Join([][]byte{ - []byte(typ), - []byte(name), - []byte(version), - }, []byte{0}) - - hash := sha256.New() - hash.Write(src) - hv := hash.Sum(nil) - return hex.EncodeToString(hv) -} - -func savePackages(ctx *model.Context, dbClient *sql.DB, typ string, packages []ftypes.Package) error { - tx, err := dbClient.Begin() - if err != nil { - return goerr.Wrap(err, "starting transaction") - } - defer utils.SafeRollback(tx) - q := db.New(tx) - - pkgSet := map[string]*ftypes.Package{} - pkgIDs := []string{} - for i, pkg := range packages { - pkgID := calcPackageID(typ, pkg.Name, pkg.Version) - pkgSet[pkgID] = &packages[i] - pkgIDs = append(pkgIDs, calcPackageID(typ, pkg.Name, pkg.Version)) - } - - exists, err := q.GetPackages(ctx, pkgIDs) - if err != nil { - return goerr.Wrap(err, "getting packages") - } - - for _, pkg := range exists { - delete(pkgSet, pkg.ID) - } - - for pkgID, pkg := range pkgSet { - if err := q.SavePackage(ctx, db.SavePackageParams{ - ID: pkgID, - TargetType: typ, - Name: pkg.Name, - Version: pkg.Version, - }); err != nil { - return goerr.Wrap(err, "saving package").With("pkgID", pkgID).With("pkg", pkg) - } - } - - if err := tx.Commit(); err != nil { - return goerr.Wrap(err, "commit transaction") - } - - return nil -} - -func saveVulnerabilities(ctx *model.Context, dbClient *sql.DB, vulns []ttypes.DetectedVulnerability) error { - tx, err := dbClient.Begin() - if err != nil { - return goerr.Wrap(err, "starting transaction") - } - defer utils.SafeRollback(tx) - q := db.New(tx) - - for _, vuln := range vulns { - data, err := json.Marshal(vuln.Vulnerability) - if err != nil { - return goerr.Wrap(err, "marshaling vulnerability").With("vuln", vuln) - } - - if err := q.SaveVulnerability(ctx, db.SaveVulnerabilityParams{ - ID: vuln.VulnerabilityID, - Title: vuln.Title, - Severity: vuln.Severity, - PublishedAt: sql.NullTime{ - Valid: vuln.PublishedDate != nil, - Time: *vuln.PublishedDate, - }, - LastModifiedAt: sql.NullTime{ - Valid: vuln.LastModifiedDate != nil, - Time: *vuln.LastModifiedDate, - }, - Data: pqtype.NullRawMessage{Valid: true, RawMessage: data}, - }); err != nil { - return goerr.Wrap(err, "saving vulnerability").With("vuln", vuln) - } - } - - if err := tx.Commit(); err != nil { - return goerr.Wrap(err, "commit transaction") - } - - return nil -} diff --git a/pkg/usecase/save_scan_report_test.go b/pkg/usecase/save_scan_report_test.go deleted file mode 100644 index e53880e6..00000000 --- a/pkg/usecase/save_scan_report_test.go +++ /dev/null @@ -1,191 +0,0 @@ -package usecase_test - -import ( - "database/sql" - "encoding/json" - "os" - "testing" - "time" - - ptypes "github.com/aquasecurity/trivy-db/pkg/types" - ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" - ttypes "github.com/aquasecurity/trivy/pkg/types" - "github.com/google/uuid" - "github.com/m-mizutani/gots/ptr" - "github.com/m-mizutani/gots/rands" - "github.com/m-mizutani/gt" - "github.com/m-mizutani/octovy/pkg/domain/model" - "github.com/m-mizutani/octovy/pkg/infra/db" - "github.com/m-mizutani/octovy/pkg/usecase" - - _ "github.com/lib/pq" -) - -func TestCalcPackageID(t *testing.T) { - hv1 := usecase.CalcPackageID("go", "pkgA", "v1.0.0") - hv2 := usecase.CalcPackageID("go", "pkgA", "v1.0.1") - hv3 := usecase.CalcPackageID("bundler", "pkgA", "v1.0.1") - hv4 := usecase.CalcPackageID("go", "pkgB", "v1.0.0") - - gt.A(t, []string{hv1, hv2, hv3, hv4}).Distinct() -} - -func newTestDB(t *testing.T) *sql.DB { - testDSN, ok := os.LookupEnv("TEST_DB_DSN") - if !ok { - t.Errorf("TEST_DB_DSN is not set") - t.FailNow() - } - dbClient := gt.R1(sql.Open("postgres", testDSN)).NoError(t) - t.Cleanup(func() { - gt.NoError(t, dbClient.Close()) - }) - - return dbClient -} - -func TestSaveScan(t *testing.T) { - salt := rands.AlphaNum(10) - report := ttypes.Report{ - ArtifactName: "github.com/m-mizutani/octovy", - ArtifactType: ttypes.ClassLangPkg, - Results: ttypes.Results{ - { - Target: "Gemfile.lock", - Class: ttypes.ClassLangPkg, - Type: "bundler", - Packages: []ftypes.Package{ - { - Name: "octokit_" + salt, - Version: "4.18.0", - }, - }, - Vulnerabilities: []ttypes.DetectedVulnerability{ - { - VulnerabilityID: "CVE-2020-1234-" + salt, - PkgName: "octokit_" + salt, - InstalledVersion: "4.18.0", - FixedVersion: "4.18.1", - Vulnerability: ptypes.Vulnerability{ - Title: "CVE-2020-1234", - Description: "test", - Severity: "HIGH", - References: []string{"https://example.com"}, - CVSS: ptypes.VendorCVSS{ - "nvd": { - V2Vector: "AV:L/AC:M/Au:N/C:C/I:C/A:C", - V3Vector: "CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", - V2Score: 6.9, - V3Score: 6.4, - }, - "redhat": { - V3Vector: "CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", - V3Score: 6.4, - }, - }, - CweIDs: []string{"CWE-1234"}, - PublishedDate: ptr.To(time.Now()), - LastModifiedDate: ptr.To(time.Now()), - }, - }, - }, - }, - }, - } - - dbClient := newTestDB(t) - ctx := model.NewContext() - meta := &usecase.GitHubRepoMetadata{ - GitHubCommit: model.GitHubCommit{ - GitHubRepo: model.GitHubRepo{ - RepoID: 287748079, - Owner: "m-mizutani", - RepoName: "octovy", - }, - CommitID: "1234567890", - }, - } - gt.NoError(t, usecase.SaveScanReportGitHubRepo(ctx, dbClient, &report, meta)) -} - -func TestUpsertVulnerability(t *testing.T) { - salt := rands.AlphaNum(10) - - now := time.Now() - - report := ttypes.Report{ - ArtifactName: "github.com/m-mizutani/octovy", - ArtifactType: ttypes.ClassLangPkg, - Results: ttypes.Results{ - { - Target: "Gemfile.lock", - Class: ttypes.ClassLangPkg, - Type: "bundler", - Packages: []ftypes.Package{ - { - Name: "octokit_" + salt, - Version: "4.18.0", - }, - }, - Vulnerabilities: []ttypes.DetectedVulnerability{ - { - VulnerabilityID: "CVE-2020-1234-" + salt, - PkgName: "octokit_" + salt, - InstalledVersion: "4.18.0", - Vulnerability: ptypes.Vulnerability{ - Title: "CVE-2020-1234", - Description: "test", - Severity: "MIDDLE", - References: []string{"https://example.com"}, - PublishedDate: ptr.To(now), - LastModifiedDate: ptr.To(now), - }, - }, - }, - }, - }, - } - - dbClient := newTestDB(t) - q := db.New(dbClient) - ctx := model.NewContext() - meta := &usecase.GitHubRepoMetadata{ - GitHubCommit: model.GitHubCommit{ - GitHubRepo: model.GitHubRepo{ - RepoID: 287748079, - Owner: "m-mizutani", - RepoName: "octovy", - }, - CommitID: uuid.NewString(), - }, - } - - t.Run("save vulnerability", func(t *testing.T) { - gt.NoError(t, usecase.SaveScanReportGitHubRepo(ctx, dbClient, &report, meta)) - vuln := gt.R1(q.GetVulnerability(ctx, "CVE-2020-1234-"+salt)).NoError(t) - gt.V(t, vuln).NotNil() - gt.V(t, vuln.Severity).Equal("MIDDLE") - }) - - t.Run("update severity, but not update last modified", func(t *testing.T) { - report.Results[0].Vulnerabilities[0].Vulnerability.Severity = "HIGH" - gt.NoError(t, usecase.SaveScanReportGitHubRepo(ctx, dbClient, &report, meta)) - vuln := gt.R1(q.GetVulnerability(ctx, "CVE-2020-1234-"+salt)).NoError(t) - gt.V(t, vuln).NotNil() - gt.V(t, vuln.Severity).Equal("MIDDLE") - }) - - t.Run("update severity and last modified", func(t *testing.T) { - report.Results[0].Vulnerabilities[0].Vulnerability.Severity = "CRITICAL" - report.Results[0].Vulnerabilities[0].Vulnerability.LastModifiedDate = ptr.To(now.Add(time.Second)) - gt.NoError(t, usecase.SaveScanReportGitHubRepo(ctx, dbClient, &report, meta)) - resp := gt.R1(q.GetVulnerability(ctx, "CVE-2020-1234-"+salt)).NoError(t) - gt.V(t, resp).NotNil() - gt.V(t, resp.Severity).Equal("CRITICAL") - - var vuln ttypes.DetectedVulnerability - gt.NoError(t, json.Unmarshal(resp.Data.RawMessage, &vuln)) - gt.V(t, vuln.LastModifiedDate.Unix()).Equal(now.Add(time.Second).Unix()) - gt.V(t, vuln.Severity).Equal("CRITICAL") - }) -} diff --git a/pkg/usecase/scan_github_repo.go b/pkg/usecase/scan_github_repo.go index 98606f00..9d301ab0 100644 --- a/pkg/usecase/scan_github_repo.go +++ b/pkg/usecase/scan_github_repo.go @@ -13,47 +13,39 @@ import ( "path/filepath" "strings" + "github.com/google/go-github/v53/github" "github.com/m-mizutani/goerr" + "github.com/m-mizutani/octovy/pkg/domain/interfaces" "github.com/m-mizutani/octovy/pkg/domain/model" + "github.com/m-mizutani/octovy/pkg/domain/model/trivy" "github.com/m-mizutani/octovy/pkg/domain/types" "github.com/m-mizutani/octovy/pkg/infra" - gh "github.com/m-mizutani/octovy/pkg/infra/gh" "github.com/m-mizutani/octovy/pkg/utils" - - ttype "github.com/aquasecurity/trivy/pkg/types" ) -type ScanGitHubRepoInput struct { - GitHubRepoMetadata - InstallID types.GitHubAppInstallID -} - -type GitHubRepoMetadata struct { - model.GitHubCommit - Branch string - IsDefaultBranch bool - BaseCommitID string - PullRequestID int -} - -func (x *ScanGitHubRepoInput) Validate() error { - if err := x.GitHubRepoMetadata.Validate(); err != nil { - return err - } - if x.InstallID == 0 { - return goerr.Wrap(types.ErrInvalidOption, "install ID is empty") - } - - return nil -} - // ScanGitHubRepo is a usecase to download a source code from GitHub and scan it with Trivy. Using GitHub App credentials to download a private repository, then the app should be installed to the repository and have read access. // After scanning, the result is stored to the database. The temporary files are removed after the scan. -func (x *useCase) ScanGitHubRepo(ctx *model.Context, input *ScanGitHubRepoInput) error { +func (x *UseCase) ScanGitHubRepo(ctx context.Context, input *model.ScanGitHubRepoInput) error { if err := input.Validate(); err != nil { return err } + // Create and finalize GitHub check + conclusion := "cancelled" + checkID, err := x.clients.GitHubApp().CreateCheckRun(ctx, input.InstallID, &input.GitHubRepo, input.CommitID) + if err != nil { + return err + } + defer func() { + opt := &github.UpdateCheckRunOptions{ + Status: github.String("completed"), + Conclusion: &conclusion, + } + if err := x.clients.GitHubApp().UpdateCheckRun(ctx, input.InstallID, &input.GitHubRepo, checkID, opt); err != nil { + utils.CtxLogger(ctx).Error("Failed to update check run", "err", err) + } + }() + // Extract zip file to local temp directory tmpDir, err := os.MkdirTemp("", fmt.Sprintf("octovy.%s.%s.%s.*", input.Owner, input.RepoName, input.CommitID)) if err != nil { @@ -65,22 +57,29 @@ func (x *useCase) ScanGitHubRepo(ctx *model.Context, input *ScanGitHubRepoInput) return err } - ctx = ctx.New(model.WithBase(context.Background())) report, err := x.scanGitHubRepo(ctx, tmpDir) if err != nil { return err } - ctx.Logger().Info("scan finished", slog.Any("input", input)) + utils.CtxLogger(ctx).Info("scan finished", "input", input, "report", report) - if err := saveScanReportGitHubRepo(ctx, x.clients.DB(), report, &input.GitHubRepoMetadata); err != nil { + if err := x.InsertScanResult(ctx, input.GitHubMetadata, *report); err != nil { return err } + if nil != x.clients.Storage() && nil != input.GitHubMetadata.PullRequest { + if err := x.CommentGitHubPR(ctx, input, report); err != nil { + return err + } + } + + conclusion = "success" + return nil } -func (x *useCase) downloadGitHubRepo(ctx *model.Context, input *ScanGitHubRepoInput, dstDir string) error { - zipURL, err := x.clients.GitHubApp().GetArchiveURL(ctx, &gh.GetArchiveURLInput{ +func (x *UseCase) downloadGitHubRepo(ctx context.Context, input *model.ScanGitHubRepoInput, dstDir string) error { + zipURL, err := x.clients.GitHubApp().GetArchiveURL(ctx, &interfaces.GetArchiveURLInput{ Owner: input.Owner, Repo: input.RepoName, CommitID: input.CommitID, @@ -113,7 +112,7 @@ func (x *useCase) downloadGitHubRepo(ctx *model.Context, input *ScanGitHubRepoIn return nil } -func (x *useCase) scanGitHubRepo(ctx *model.Context, codeDir string) (*ttype.Report, error) { +func (x *UseCase) scanGitHubRepo(ctx context.Context, codeDir string) (*trivy.Report, error) { // Scan local directory tmpResult, err := os.CreateTemp("", "octovy_result.*.json") if err != nil { @@ -137,12 +136,12 @@ func (x *useCase) scanGitHubRepo(ctx *model.Context, codeDir string) (*ttype.Rep return nil, goerr.Wrap(err, "failed to scan local directory") } - var report ttype.Report + var report trivy.Report if err := unmarshalFile(tmpResult.Name(), &report); err != nil { return nil, err } - ctx.Logger().Info("Scan result", slog.Any("report", tmpResult.Name())) + utils.CtxLogger(ctx).Info("Scan result", slog.Any("report", tmpResult.Name())) return &report, nil } @@ -161,7 +160,7 @@ func unmarshalFile(path string, v any) error { return nil } -func downloadZipFile(ctx *model.Context, httpClient infra.HTTPClient, zipURL *url.URL, w io.Writer) error { +func downloadZipFile(ctx context.Context, httpClient infra.HTTPClient, zipURL *url.URL, w io.Writer) error { zipReq, err := http.NewRequestWithContext(ctx, http.MethodGet, zipURL.String(), nil) if err != nil { return goerr.Wrap(err, "failed to create request for zip file").With("url", zipURL) @@ -185,7 +184,7 @@ func downloadZipFile(ctx *model.Context, httpClient infra.HTTPClient, zipURL *ur return nil } -func extractZipFile(ctx *model.Context, src, dst string) error { +func extractZipFile(ctx context.Context, src, dst string) error { zipFile, err := zip.OpenReader(src) if err != nil { return goerr.Wrap(err).With("file", src) @@ -202,7 +201,7 @@ func extractZipFile(ctx *model.Context, src, dst string) error { return nil } -func extractCode(ctx *model.Context, f *zip.File, dst string) error { +func extractCode(_ context.Context, f *zip.File, dst string) error { if f.FileInfo().IsDir() { return nil } diff --git a/pkg/usecase/scan_github_repo_test.go b/pkg/usecase/scan_github_repo_test.go index de122e66..21a3805b 100644 --- a/pkg/usecase/scan_github_repo_test.go +++ b/pkg/usecase/scan_github_repo_test.go @@ -1,9 +1,8 @@ package usecase_test import ( - "database/sql" + "context" _ "embed" - "fmt" "os" "strconv" @@ -13,10 +12,15 @@ import ( "net/url" "testing" + "cloud.google.com/go/bigquery" + "github.com/google/go-github/v53/github" + "github.com/m-mizutani/gt" + "github.com/m-mizutani/octovy/pkg/domain/interfaces" "github.com/m-mizutani/octovy/pkg/domain/model" "github.com/m-mizutani/octovy/pkg/domain/types" "github.com/m-mizutani/octovy/pkg/infra" + "github.com/m-mizutani/octovy/pkg/infra/bq" "github.com/m-mizutani/octovy/pkg/infra/gh" "github.com/m-mizutani/octovy/pkg/usecase" "github.com/m-mizutani/octovy/pkg/utils" @@ -29,29 +33,37 @@ var testCodeZip []byte var testTrivyResult []byte func TestScanGitHubRepo(t *testing.T) { - mockGH := &ghMock{} + mockGH := &interfaces.GitHubMock{} mockHTTP := &httpMock{} mockTrivy := &trivyMock{} - testDB := newTestDB(t) + mockBQ := &bq.Mock{} + mockStorage := interfaces.NewStorageMock() uc := usecase.New(infra.New( infra.WithGitHubApp(mockGH), infra.WithHTTPClient(mockHTTP), infra.WithTrivy(mockTrivy), - infra.WithDB(testDB), + infra.WithBigQuery(mockBQ), + infra.WithStorage(mockStorage), )) - ctx := model.NewContext() + ctx := context.Background() - mockGH.mockGetArchiveURL = func(ctx *model.Context, input *gh.GetArchiveURLInput) (*url.URL, error) { + mockGH.MockGetArchiveURL = func(ctx context.Context, input *interfaces.GetArchiveURLInput) (*url.URL, error) { gt.V(t, input.Owner).Equal("m-mizutani") gt.V(t, input.Repo).Equal("octovy") - gt.V(t, input.CommitID).Equal("1234567890") + gt.V(t, input.CommitID).Equal("f7c8851da7c7fcc46212fccfb6c9c4bda520f1ca") gt.V(t, input.InstallID).Equal(12345) resp := gt.R1(url.Parse("https://example.com/some/url.zip")).NoError(t) return resp, nil } + mockGH.MockCreateCheckRun = func(ctx context.Context, id types.GitHubAppInstallID, repo *model.GitHubRepo, commit string) (int64, error) { + return 0, nil + } + mockGH.MockUpdateCheckRun = func(ctx context.Context, id types.GitHubAppInstallID, repo *model.GitHubRepo, checkID int64, opt *github.UpdateCheckRunOptions) error { + return nil + } mockHTTP.mockDo = func(req *http.Request) (*http.Response, error) { gt.V(t, req.URL.String()).Equal("https://example.com/some/url.zip") @@ -63,7 +75,7 @@ func TestScanGitHubRepo(t *testing.T) { return resp, nil } - mockTrivy.mockRun = func(ctx *model.Context, args []string) error { + mockTrivy.mockRun = func(ctx context.Context, args []string) error { gt.A(t, args). Contain([]string{"--format", "json"}). Contain([]string{"--list-all-pkgs"}) @@ -81,34 +93,54 @@ func TestScanGitHubRepo(t *testing.T) { return nil } - gt.NoError(t, uc.ScanGitHubRepo(ctx, &usecase.ScanGitHubRepoInput{ - GitHubRepoMetadata: usecase.GitHubRepoMetadata{ + var calledBQCreateTable int + mockBQ.FnCreateTable = func(ctx context.Context, table types.BQTableID, md *bigquery.TableMetadata) error { + calledBQCreateTable++ + gt.Equal(t, table, "scans") + return nil + } + + mockBQ.FnGetMetadata = func(ctx context.Context, table types.BQTableID) (*bigquery.TableMetadata, error) { + return nil, nil + } + + var calledBQInsert int + mockBQ.FnInsert = func(ctx context.Context, tableID types.BQTableID, schema bigquery.Schema, data any) error { + calledBQInsert++ + return nil + } + + gt.NoError(t, uc.ScanGitHubRepo(ctx, &model.ScanGitHubRepoInput{ + GitHubMetadata: model.GitHubMetadata{ GitHubCommit: model.GitHubCommit{ GitHubRepo: model.GitHubRepo{ RepoID: 12345, Owner: "m-mizutani", RepoName: "octovy", }, - CommitID: "1234567890", + CommitID: "f7c8851da7c7fcc46212fccfb6c9c4bda520f1ca", + Branch: "main", }, }, InstallID: 12345, })) -} + gt.Equal(t, calledBQCreateTable, 1) + gt.Equal(t, calledBQInsert, 1) -type ghMock struct { - mockGetArchiveURL func(ctx *model.Context, input *gh.GetArchiveURLInput) (*url.URL, error) -} + var commitScan *model.Scan + gt.NoError(t, mockStorage.Unmarshal("m-mizutani/octovy/commit/f7c8851da7c7fcc46212fccfb6c9c4bda520f1ca/scan.json.gz", &commitScan)) + gt.Equal(t, commitScan.GitHub.Owner, "m-mizutani") -func (x *ghMock) GetArchiveURL(ctx *model.Context, input *gh.GetArchiveURLInput) (*url.URL, error) { - return x.mockGetArchiveURL(ctx, input) + var branchScan *model.Scan + gt.NoError(t, mockStorage.Unmarshal("m-mizutani/octovy/branch/main/scan.json.gz", &branchScan)) + gt.Equal(t, branchScan.GitHub.Owner, "m-mizutani") } type trivyMock struct { - mockRun func(ctx *model.Context, args []string) error + mockRun func(ctx context.Context, args []string) error } -func (x *trivyMock) Run(ctx *model.Context, args []string) error { +func (x *trivyMock) Run(ctx context.Context, args []string) error { return x.mockRun(ctx, args) } @@ -121,57 +153,26 @@ func (x *httpMock) Do(req *http.Request) (*http.Response, error) { } func TestScanGitHubRepoWithData(t *testing.T) { + if _, ok := os.LookupEnv("TEST_SCAN_GITHUB_REPO"); !ok { t.Skip("TEST_SCAN_GITHUB_REPO is not set") } // Setting up GitHub App - strAppID, ok := os.LookupEnv("OCTOVY_GITHUB_APP_ID") - if !ok { - t.Error("OCTOVY_GITHUB_APP_ID is not set") - } - privateKey, ok := os.LookupEnv("OCTOVY_GITHUB_APP_PRIVATE_KEY") - if !ok { - t.Error("OCTOVY_GITHUB_APP_PRIVATE_KEY is not set") - } + strAppID := utils.LoadEnv(t, "TEST_OCTOVY_GITHUB_APP_ID") + privateKey := utils.LoadEnv(t, "TEST_OCTOVY_GITHUB_APP_PRIVATE_KEY") + appID := gt.R1(strconv.ParseInt(strAppID, 10, 64)).NoError(t) ghApp := gt.R1(gh.New(types.GitHubAppID(appID), types.GitHubAppPrivateKey(privateKey))).NoError(t) - // Setting up database - dbUser, ok := os.LookupEnv("OCTOVY_DB_USER") - if !ok { - t.Error("OCTOVY_DB_USER is not set") - } - dbPass, ok := os.LookupEnv("OCTOVY_DB_PASSWORD") - if !ok { - t.Error("OCTOVY_DB_PASS is not set") - } - dbName, ok := os.LookupEnv("OCTOVY_DB_NAME") - if !ok { - t.Error("OCTOVY_DB_NAME is not set") - } - dbPort, ok := os.LookupEnv("OCTOVY_DB_PORT") - if !ok { - t.Error("OCTOVY_DB_PORT is not set") - } - dsn := fmt.Sprintf("user=%s password=%s dbname=%s port=%s sslmode=disable", dbUser, dbPass, dbName, dbPort) - - dbClient := gt.R1(sql.Open("postgres", dsn)).NoError(t) - defer utils.SafeClose(dbClient) - - if t.Failed() { - t.FailNow() - } - uc := usecase.New(infra.New( infra.WithGitHubApp(ghApp), - infra.WithDB(dbClient), )) - ctx := model.NewContext() + ctx := context.Background() - gt.NoError(t, uc.ScanGitHubRepo(ctx, &usecase.ScanGitHubRepoInput{ - GitHubRepoMetadata: usecase.GitHubRepoMetadata{ + gt.NoError(t, uc.ScanGitHubRepo(ctx, &model.ScanGitHubRepoInput{ + GitHubMetadata: model.GitHubMetadata{ GitHubCommit: model.GitHubCommit{ GitHubRepo: model.GitHubRepo{ RepoID: 41633205, @@ -184,3 +185,119 @@ func TestScanGitHubRepoWithData(t *testing.T) { InstallID: 41633205, })) } + +func TestScanGitHubRepoWithPR(t *testing.T) { + mockGH := &interfaces.GitHubMock{} + mockHTTP := &httpMock{} + mockTrivy := &trivyMock{} + mockBQ := &bq.Mock{} + mockStorage := interfaces.NewStorageMock() + + uc := usecase.New(infra.New( + infra.WithGitHubApp(mockGH), + infra.WithHTTPClient(mockHTTP), + infra.WithTrivy(mockTrivy), + infra.WithBigQuery(mockBQ), + infra.WithStorage(mockStorage), + )) + + ctx := context.Background() + + mockHTTP.mockDo = func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(testCodeZip)), + }, nil + } + + mockTrivy.mockRun = func(ctx context.Context, args []string) error { + for i := range args { + if args[i] == "--output" { + fd := gt.R1(os.Create(args[i+1])).NoError(t) + gt.R1(fd.Write(testTrivyResult)).NoError(t) + gt.NoError(t, fd.Close()) + return nil + } + } + t.Fatalf("no --output option") + return nil + } + + var calledBQCreateTable int + mockBQ.FnCreateTable = func(ctx context.Context, table types.BQTableID, md *bigquery.TableMetadata) error { + calledBQCreateTable++ + return nil + } + mockBQ.FnGetMetadata = func(ctx context.Context, table types.BQTableID) (*bigquery.TableMetadata, error) { + return nil, nil + } + var calledBQInsert int + mockBQ.FnInsert = func(ctx context.Context, tableID types.BQTableID, schema bigquery.Schema, data any) error { + calledBQInsert++ + return nil + } + + mockGH.MockGetArchiveURL = func(ctx context.Context, input *interfaces.GetArchiveURLInput) (*url.URL, error) { + u := gt.R1(url.Parse("https://example.com/some/url.zip")).NoError(t) + return u, nil + } + var calledMockListIssueComments int + mockGH.MockListIssueComments = func(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, prID int) ([]*model.GitHubIssueComment, error) { + calledMockListIssueComments++ + return nil, nil + } + var calledMockCreateIssueComment int + mockGH.MockCreateIssueComment = func(ctx context.Context, repo *model.GitHubRepo, id types.GitHubAppInstallID, prID int, body string) error { + calledMockCreateIssueComment++ + return nil + } + var calledMockGHCreateCheckRun int + mockGH.MockCreateCheckRun = func(ctx context.Context, id types.GitHubAppInstallID, repo *model.GitHubRepo, commit string) (int64, error) { + calledMockGHCreateCheckRun++ + return 5, nil + } + var calledMockGHUpdateCheckRun int + mockGH.MockUpdateCheckRun = func(ctx context.Context, id types.GitHubAppInstallID, repo *model.GitHubRepo, checkID int64, opt *github.UpdateCheckRunOptions) error { + gt.Equal(t, checkID, 5) + gt.Equal(t, *opt.Status, "completed") + gt.Equal(t, *opt.Conclusion, "success") + calledMockGHUpdateCheckRun++ + return nil + } + + gt.NoError(t, uc.ScanGitHubRepo(ctx, &model.ScanGitHubRepoInput{ + GitHubMetadata: model.GitHubMetadata{ + GitHubCommit: model.GitHubCommit{ + GitHubRepo: model.GitHubRepo{ + RepoID: 12345, + Owner: "m-mizutani", + RepoName: "octovy", + }, + CommitID: "f7c8851da7c7fcc46212fccfb6c9c4bda520f1ca", + Branch: "main", + }, + PullRequest: &model.GitHubPullRequest{ + Number: 123, + ID: 12345, + BaseBranch: "main", + BaseCommitID: "0f2324c367815ec3d928d21b892ce0ed9963aef3", + }, + }, + InstallID: 12345, + })) + + gt.Equal(t, calledBQCreateTable, 1) + gt.Equal(t, calledBQInsert, 1) + gt.Equal(t, calledMockListIssueComments, 1) + gt.Equal(t, calledMockCreateIssueComment, 1) + gt.Equal(t, calledMockGHCreateCheckRun, 1) + gt.Equal(t, calledMockGHUpdateCheckRun, 1) + + var commitScan *model.Scan + gt.NoError(t, mockStorage.Unmarshal("m-mizutani/octovy/commit/f7c8851da7c7fcc46212fccfb6c9c4bda520f1ca/scan.json.gz", &commitScan)) + gt.Equal(t, commitScan.GitHub.Owner, "m-mizutani") + + var branchScan *model.Scan + gt.NoError(t, mockStorage.Unmarshal("m-mizutani/octovy/branch/main/scan.json.gz", &branchScan)) + gt.Equal(t, branchScan.GitHub.Owner, "m-mizutani") +} diff --git a/pkg/usecase/templates/comment_body.md b/pkg/usecase/templates/comment_body.md new file mode 100644 index 00000000..8a88d694 --- /dev/null +++ b/pkg/usecase/templates/comment_body.md @@ -0,0 +1,67 @@ +{{ .Signature }} +{{ if eq .Metadata.TotalVulnCount 0 }} +🎉 **No vulnerability detected** 🎉 +{{ else if eq .Metadata.FixableVulnCount 0 }} +👍 **No fixable vulnerability detected** 👍 +{{ end }} + +{{ if .Added }} +## 🚨 New Vulnerabilities +{{ range .Added }} +### {{ .Target }} +{{ range .Vulnerabilities }} +
+{{ .VulnerabilityID }}: {{ .Title }} ({{.Severity}}) + +- **PkgName**: {{ if .PkgName }}`{{ .PkgName }}`{{ else }}N/A{{ end }} +- **Installed Version**: {{ if .InstalledVersion }}`{{ .InstalledVersion }}`{{ else }}N/A{{ end }} +- **Fixed Version**: {{ if .FixedVersion }}`{{ .FixedVersion }}`{{ else }}N/A{{ end }} +- **Status**: {{ if .Status }}`{{ .Status }}`{{ else }}N/A{{ end }} +- **Severity**: {{ if .Severity }}`{{ .Severity }}`{{ else }}N/A{{ end }} + +#### Description + +{{ .Description }} + +#### References +{{ range .References }} +- [{{ . }}]({{ . }}){{ end }} +
+{{ end }}{{ end }}{{ end }} + +{{ if .Fixed }} +## ✅ Fix Vulnerabilities +{{ range .Fixed }} +### {{ .Target }} +{{ range .Vulnerabilities }} +
+{{ .VulnerabilityID }}: {{ .Title }} ({{.Severity}}) + +- **PkgName**: {{ if .PkgName }}`{{ .PkgName }}`{{ else }}N/A{{ end }} +- **Installed Version**: {{ if .InstalledVersion }}`{{ .InstalledVersion }}`{{ else }}N/A{{ end }} +- **Fixed Version**: {{ if .FixedVersion }}`{{ .FixedVersion }}`{{ else }}N/A{{ end }} +- **Status**: {{ if .Status }}`{{ .Status }}`{{ else }}N/A{{ end }} +- **Severity**: {{ if .Severity }}`{{ .Severity }}`{{ else }}N/A{{ end }} + +#### Description + +{{ .Description }} + +#### References + +{{ range .References }} +- [{{ . }}]({{ . }}){{ end }} +
+{{ end }}{{ end }}{{ end }} + +{{ if ne .Metadata.TotalVulnCount 0 }} +## ⚠️ All detected vulnerabilities +{{ range .Report.Results }} +
+{{ .Target }}: ({{ .Vulnerabilities | len }}) + +{{ range .Vulnerabilities }}- {{ .VulnerabilityID }}: ( `{{ .PkgName }}` ) {{ .Title }} +{{ end }} +
+{{ end }} +{{ end }} diff --git a/pkg/usecase/usecase.go b/pkg/usecase/usecase.go index bb20633e..29a22d3f 100644 --- a/pkg/usecase/usecase.go +++ b/pkg/usecase/usecase.go @@ -1,20 +1,32 @@ package usecase import ( - "github.com/m-mizutani/octovy/pkg/domain/model" + "github.com/m-mizutani/octovy/pkg/domain/types" "github.com/m-mizutani/octovy/pkg/infra" ) -type UseCase interface { - ScanGitHubRepo(ctx *model.Context, input *ScanGitHubRepoInput) error -} - -type useCase struct { +type UseCase struct { + tableID types.BQTableID clients *infra.Clients } -func New(clients *infra.Clients) UseCase { - return &useCase{ +func New(clients *infra.Clients, options ...Option) *UseCase { + uc := &UseCase{ + tableID: "scans", clients: clients, } + + for _, opt := range options { + opt(uc) + } + + return uc +} + +type Option func(*UseCase) + +func WithBigQueryTableID(tableID types.BQTableID) Option { + return func(x *UseCase) { + x.tableID = tableID + } } diff --git a/pkg/utils/context.go b/pkg/utils/context.go new file mode 100644 index 00000000..d9c929db --- /dev/null +++ b/pkg/utils/context.go @@ -0,0 +1,52 @@ +package utils + +import ( + "context" + "log/slog" + "time" + + "github.com/m-mizutani/octovy/pkg/domain/types" +) + +type ctxRequestIDKey struct{} + +// CtxRequestID returns request ID from context. If request ID is not set, return new request ID and context with it +func CtxRequestID(ctx context.Context) (types.RequestID, context.Context) { + if id, ok := ctx.Value(ctxRequestIDKey{}).(types.RequestID); ok { + return id, ctx + } + + newID := types.NewRequestID() + return newID, context.WithValue(ctx, ctxRequestIDKey{}, newID) +} + +type ctxLoggerKey struct{} + +// WithLogger returns a new context with logger +func CtxWithLogger(ctx context.Context, logger *slog.Logger) context.Context { + return context.WithValue(ctx, ctxLoggerKey{}, logger) +} + +// CtxLogger returns logger from context. If logger is not set, return default logger +func CtxLogger(ctx context.Context) *slog.Logger { + if l, ok := ctx.Value(ctxLoggerKey{}).(*slog.Logger); ok { + return l + } + return logger +} + +type ctxTimeKey struct{} +type TimeFunc func() time.Time + +// CtxTime returns time from context. If time is not set, return current time and context with it +func CtxTime(ctx context.Context) time.Time { + if t, ok := ctx.Value(ctxTimeKey{}).(TimeFunc); ok { + return t() + } + return time.Now() +} + +// CtxWithTime returns a new context with time function +func CtxWithTime(ctx context.Context, timeFunc TimeFunc) context.Context { + return context.WithValue(ctx, ctxTimeKey{}, timeFunc) +} diff --git a/pkg/utils/error.go b/pkg/utils/error.go new file mode 100644 index 00000000..f146b777 --- /dev/null +++ b/pkg/utils/error.go @@ -0,0 +1,27 @@ +package utils + +import ( + "context" + "fmt" + + "github.com/getsentry/sentry-go" + "github.com/m-mizutani/goerr" +) + +func HandleError(ctx context.Context, msg string, err error) { + // Sending error to Sentry + hub := sentry.CurrentHub().Clone() + hub.ConfigureScope(func(scope *sentry.Scope) { + if goErr := goerr.Unwrap(err); goErr != nil { + for k, v := range goErr.Values() { + scope.SetExtra(fmt.Sprintf("%v", k), v) + } + } + }) + evID := hub.CaptureException(err) + + CtxLogger(ctx).Error(msg, + "error", err, + "sentry.EventID", evID, + ) +} diff --git a/pkg/utils/hash.go b/pkg/utils/hash.go new file mode 100644 index 00000000..01776dd0 --- /dev/null +++ b/pkg/utils/hash.go @@ -0,0 +1,13 @@ +package utils + +import ( + "crypto/sha256" + "encoding/hex" +) + +func HashBranch(branch string) string { + h := sha256.New() + h.Write([]byte(branch)) + v := hex.EncodeToString(h.Sum(nil)) + return v +} diff --git a/pkg/utils/hash_test.go b/pkg/utils/hash_test.go new file mode 100644 index 00000000..778a6c6c --- /dev/null +++ b/pkg/utils/hash_test.go @@ -0,0 +1,13 @@ +package utils_test + +import ( + "testing" + + "github.com/m-mizutani/gt" + "github.com/m-mizutani/octovy/pkg/utils" +) + +func TestHashBranch(t *testing.T) { + v := utils.HashBranch("test") + gt.Equal(t, v, "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08") +} diff --git a/pkg/utils/safe_differ.go b/pkg/utils/safe.go similarity index 94% rename from pkg/utils/safe_differ.go rename to pkg/utils/safe.go index d08c3f3b..f76931ed 100644 --- a/pkg/utils/safe_differ.go +++ b/pkg/utils/safe.go @@ -11,6 +11,9 @@ import ( func SafeClose(closer io.Closer) { if closer != nil { if err := closer.Close(); err != nil { + if err == io.EOF { + return + } logger.Warn("Fail to close resource", slog.Any("error", err)) } } diff --git a/pkg/utils/test.go b/pkg/utils/test.go new file mode 100644 index 00000000..49b21498 --- /dev/null +++ b/pkg/utils/test.go @@ -0,0 +1,55 @@ +package utils + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" +) + +// LoadEnv loads environment variable and return its value for test code. If the variable is not set, it skips the test. +func LoadEnv(t *testing.T, name string) string { + t.Helper() + v, ok := os.LookupEnv(name) + if !ok { + t.Skipf("Skip test because %s is not set", name) + } + + return v +} + +// LoadJson loads JSON file and decode it into v for test code. If it fails, it fails the test. +func LoadJson(t *testing.T, path string, v interface{}) { + t.Helper() + fp, err := os.Open(filepath.Clean(path)) + if err != nil { + t.Fatalf("Failed to open file: %s", path) + } + defer SafeClose(fp) + + if err := json.NewDecoder(fp).Decode(v); err != nil { + t.Fatalf("Failed to decode JSON: %s", err) + } +} + +// DumpJson dumps v into JSON file for test code. If it fails, it fails the test. +func DumpJson(t *testing.T, path string, v interface{}) { + if t != nil { + t.Helper() + } + fp, err := os.Create(filepath.Clean(path)) + if err != nil { + if t != nil { + t.Fatalf("Failed to create file: %s", path) + } + panic(err) + } + defer SafeClose(fp) + + if err := json.NewEncoder(fp).Encode(v); err != nil { + if t != nil { + t.Fatalf("Failed to encode JSON: %s", err) + } + panic(err) + } +} diff --git a/scripts/reset_db.sh b/scripts/reset_db.sh deleted file mode 100755 index 7bfc0f4b..00000000 --- a/scripts/reset_db.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -export CONTAINER_NAME=octovy-db - -if [ -z "$POSTGRES_USER" ]; then - echo "Error: POSTGRES_USER environment variable is not set" - exit 1 -fi - -if [ -z "$POSTGRES_PASSWORD" ]; then - echo "Error: POSTGRES_PASSWORD environment variable is not set" - exit 1 -fi - -if [ -z "$POSTGRES_DB" ]; then - echo "Error: POSTGRES_DB environment variable is not set" - exit 1 -fi - -export PGPASSWORD=$POSTGRES_PASSWORD - -if [ -z "$DO_NOT_START_CONTAINER" ]; then - pid=$(finch ps -q -f "name=$CONTAINER_NAME") - - if [ "$pid" != "" ]; then - echo "Stopping local DB... $pid" - finch kill "$pid" - fi - - pid=$(finch ps -q -a -f "name=$CONTAINER_NAME") - - if [ "$pid" != "" ]; then - echo "Removing local DB... $pid" - finch rm "$pid" - fi - - echo "Starting local DB..." - finch run \ - -e POSTGRES_USER=${POSTGRES_USER} \ - -e POSTGRES_PASSWORD=${POSTGRES_PASSWORD} \ - -e POSTGRES_DB=${POSTGRES_DB} \ - -p 6432:5432 \ - -d \ - --name $CONTAINER_NAME \ - postgres:14 -fi - -# Wait for the DB to start -while true; do - psql -h localhost -p 6432 -U ${POSTGRES_USER} ${POSTGRES_DB} -c "SELECT 1;" > /dev/null 2>&1 - if [ $? -eq 0 ]; then - echo "Connected" - break - else - echo "Connection failed. Retrying in 1 second..." - sleep 1 - fi -done - -psqldef -U ${POSTGRES_USER} -p 6432 -h localhost -f database/schema.sql ${POSTGRES_DB} diff --git a/sqlc.yaml b/sqlc.yaml deleted file mode 100644 index bc4bcc3c..00000000 --- a/sqlc.yaml +++ /dev/null @@ -1,12 +0,0 @@ -version: "2" -sql: - - engine: "postgresql" - queries: "database/query.sql" - schema: "database/schema.sql" - gen: - go: - package: "db" - out: "pkg/infra/db" - #overrides: - #- column: delete_requests.id - # go_type: "github.com/ubie-inc/destroyer/pkg/domain/types.DeleteRequestID"