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.
+
+
## 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"