mirror of
https://github.com/lukaszraczylo/gohoarder.git
synced 2026-10-12 00:45:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80928c6e21 | ||
|
|
5bcbe08573 | ||
|
|
5c86b9a5e6 | ||
|
|
e246206e9c | ||
|
|
3c33bcdd61 | ||
|
|
cc7d822a0a | ||
|
|
30e113fbbe | ||
|
|
cbceb6101b | ||
|
|
3b6d2e7258 | ||
|
|
328f764024 | ||
|
|
8f5ed9c972 | ||
|
|
21d004eae9 | ||
|
|
436633bc68 | ||
|
|
8fae680cbe | ||
|
|
b172788741 | ||
|
|
48c12a4869 | ||
|
|
8396284de3 | ||
|
|
d57a21817b | ||
|
|
1ebb4cfd95 | ||
|
|
8112b4859f | ||
|
|
a412b61d8d | ||
|
|
cbfb16aebd | ||
|
|
854245c4f3 | ||
|
|
d96e724755 | ||
|
|
d0b3d82508 | ||
|
|
6d4ce976c7 | ||
|
|
d760757c46 | ||
|
|
67d87ed66b | ||
|
|
22fc1b454b | ||
|
|
2c483445a1 | ||
|
|
f5beaea3e0 | ||
|
|
4d61ed1557 | ||
|
|
f1bcfa02f5 | ||
|
|
16e09c0515 | ||
|
|
0ff7719d8a | ||
|
|
1d7ee6c27b | ||
|
|
e00233943b | ||
|
|
6c2448bb03 | ||
|
|
a511c380ba | ||
|
|
fa66501e5c | ||
|
|
35ea5b8a5b | ||
|
|
e5d7242dc1 | ||
|
|
3c994687ff | ||
|
|
171386f7a7 | ||
|
|
edeb2eca0c | ||
|
|
7fb596aafd | ||
|
|
eb4517a45d | ||
|
|
fa1b8cf6c4 | ||
|
|
a5ac6f41e2 | ||
|
|
7de67d6a7a | ||
|
|
db5dfef2d5 | ||
|
|
b6a0a0800f | ||
|
|
36b99d528e | ||
|
|
601455cb9b | ||
|
|
e0a489e471 | ||
|
|
aa4b79ab0c | ||
|
|
8db5aa099b | ||
|
|
47f63e3b06 | ||
|
|
43f23900e0 | ||
|
|
3a113557cd | ||
|
|
22163595fd | ||
|
|
4dc7e2fcfb | ||
|
|
557ebc8410 | ||
|
|
2c82cd8a7a | ||
|
|
47cc2900a6 | ||
|
|
c6e7c0d4c8 | ||
|
|
c6ad90f742 | ||
|
|
dfc7d0c917 | ||
|
|
4442cb3062 | ||
|
|
3ce6d274d5 | ||
|
|
ce7cb769b3 | ||
|
|
f3a210fd8d | ||
|
|
c7f05c426f | ||
|
|
bd3e8f024c | ||
|
|
c390074f1c | ||
|
|
43d39bb07a | ||
|
|
a95cd85155 |
@@ -14,6 +14,6 @@ jobs:
|
||||
autoupdate:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-autoupdate.yaml@main
|
||||
with:
|
||||
go-version: ">=1.25"
|
||||
go-version: ">=1.27"
|
||||
release-workflow: "release.yaml"
|
||||
secrets: inherit
|
||||
|
||||
@@ -9,8 +9,13 @@ on:
|
||||
- "**"
|
||||
- "!main"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
pr-checks:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-pr.yaml@main
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: ">=1.27"
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
release:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-release-cgo.yaml@main
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: ">=1.27"
|
||||
# Enable frontend build
|
||||
node-enabled: true
|
||||
node-version: "20"
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: ">=1.27"
|
||||
|
||||
- name: Run benchmarks
|
||||
run: go test -bench=. -benchmem ./... -run=^# | tee output.txt
|
||||
@@ -59,19 +59,9 @@ jobs:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
comment-on-alert: true
|
||||
summary-always: true
|
||||
auto-push: false
|
||||
auto-push: true
|
||||
benchmark-data-dir-path: "docs/bench"
|
||||
|
||||
- name: Push benchmark results
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add docs/bench
|
||||
git diff --staged --quiet || git commit -m "Update benchmark results"
|
||||
git push origin main
|
||||
|
||||
publish-helm-chart:
|
||||
name: Publish Helm Chart
|
||||
needs: release
|
||||
|
||||
+10
-10
@@ -11,13 +11,13 @@
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"axios": "^1.13.2",
|
||||
"@vueuse/core": "^15.0.0",
|
||||
"axios": "^1.20.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-vue-next": "^0.562.0",
|
||||
"marked": "^17.0.1",
|
||||
"pinia": "^3.0.4",
|
||||
"lucide-vue-next": "^1.0.0",
|
||||
"marked": "^18.0.0",
|
||||
"pinia": "^4.0.0",
|
||||
"reka-ui": "^2.7.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
@@ -27,17 +27,17 @@
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/jest-dom": "^7.0.0",
|
||||
"@testing-library/vue": "^8.1.0",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"jsdom": "^27.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
"jsdom": "^30.0.0",
|
||||
"postcss": "^8.5.23",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.0",
|
||||
"vitest": "^4.0.16",
|
||||
"vite": "^7.3.5",
|
||||
"vitest": "^5.0.3",
|
||||
"vue-tsc": "^3.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1071
-1164
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
vue-demi: true
|
||||
# Patched versions of transitive dependencies that Dependabot cannot raise on its own.
|
||||
overrides:
|
||||
"minimatch@<9.0.9": ^10.0.0
|
||||
"esbuild@<0.28.2": ^0.28.2
|
||||
"postcss-selector-parser@<6.1.4": ^7.0.0
|
||||
@@ -1,32 +1,33 @@
|
||||
module github.com/lukaszraczylo/gohoarder
|
||||
|
||||
go 1.25.5
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.29
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.28
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/goccy/go-json v0.10.6
|
||||
github.com/gofiber/fiber/v2 v2.52.14
|
||||
github.com/aws/aws-sdk-go-v2 v1.47.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.33.6
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.6
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.0
|
||||
github.com/cloudsoda/go-smb2 v0.0.0-20260918041005-0c5d69b69701
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.7
|
||||
github.com/go-sql-driver/mysql v1.10.1
|
||||
github.com/goccy/go-json v0.11.2
|
||||
github.com/gofiber/fiber/v2 v2.52.15
|
||||
github.com/gofiber/fiber/v3 v3.5.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hirochachacha/go-smb2 v1.1.0
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/prometheus/client_golang v1.24.1
|
||||
github.com/rs/zerolog v1.35.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/testcontainers/testcontainers-go v0.40.0
|
||||
github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0
|
||||
golang.org/x/crypto v0.54.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/time v0.15.0
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/testcontainers/testcontainers-go v0.44.0
|
||||
github.com/testcontainers/testcontainers-go/modules/mysql v0.44.0
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.44.0
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/sync v0.23.0
|
||||
golang.org/x/time v0.16.0
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/driver/postgres v1.6.3
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.2
|
||||
)
|
||||
@@ -34,101 +35,106 @@ require (
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20260917205352-e937bb47801a // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/andybalholm/brotli v1.2.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.4.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.32.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.44.0 // indirect
|
||||
github.com/aws/smithy-go v1.27.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.10.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.38.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.51.1 // indirect
|
||||
github.com/aws/smithy-go v1.28.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v7 v7.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v7 v7.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/log v0.2.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.8.1 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/ebitengine/purego v0.11.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.1.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||
github.com/geoffgarside/ber v1.2.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.4 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.10.0 // indirect
|
||||
github.com/jackc/pgx/v5 v5.11.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/gofork v1.7.6 // indirect
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/klauspost/compress v1.19.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 // indirect
|
||||
github.com/magiconair/properties v1.18.12 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.47 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.30 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.52 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.2.0 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/moby/go-archive v0.3.3 // indirect
|
||||
github.com/moby/moby/api v1.56.0 // indirect
|
||||
github.com/moby/moby/client v0.6.0 // indirect
|
||||
github.com/moby/patternmatcher v0.6.1 // indirect
|
||||
github.com/moby/sys/sequential v0.7.0 // indirect
|
||||
github.com/moby/sys/user v0.4.1 // indirect
|
||||
github.com/moby/sys/userns v0.2.1 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.1.0 // indirect
|
||||
github.com/molecule-man/go-brrr v1.1.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.70.0 // indirect
|
||||
github.com/prometheus/procfs v0.21.1 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20260916203055-22a1a467d9f0 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.72.0 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.26.9 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.4.0 // indirect
|
||||
github.com/tklauser/numcpus v0.12.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.72.0 // indirect
|
||||
github.com/valyala/fasthttp v1.74.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 // indirect
|
||||
go.opentelemetry.io/otel v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.46.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/net v0.59.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/tools v0.50.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
)
|
||||
|
||||
@@ -4,135 +4,151 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20260917205352-e937bb47801a h1:4hxax9ktNjSDoFn1tZSeL6gXMTRMpO0FzjdKfT01jgY=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20260917205352-e937bb47801a/go.mod h1:3EWSSOZ50kb+arhww0qIaEXHToib/3FjBj9Jj5lcP5g=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
|
||||
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek=
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 h1:3IZY0XAJquT3aHzbkHfPzy4ACPcEjVG0x87KOwtpqGY=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14/go.mod h1:zwM6veDkhGgQFqkBy+uT28AAYpLu+uFMlPl+rCg/73E=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.29 h1:BcMHHnpiWKogf+gGfpj3K1w+Sktz29XDo/cPSAPO3FU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.29/go.mod h1:+Kbhn8Es4kPUph3F/0W7avykytc+Jh2Ld9/msv9ljV4=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.28 h1:zTXJSsNcoO91/mTXsZoYf0AK8dvNPiA58/VtyGXR+wM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.28/go.mod h1:Kd9E0JzDBW/q1xbsHFrev/GnbAf5J0Ng8xoyc7HZ91Q=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 h1:/hi1JADLEW9YYryEz1w4GQu0EtP23pP553Cf9KgsDV4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30/go.mod h1:/3AOgy4K17Dm4ucMZVC/MJkzy5kmfKUcINRHZyo0koQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 h1:3GUprIsfmGcC5SACIyB0e7E0BM1O1b3Erl5CePYIAeQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31/go.mod h1:7PuV1yl5e2xnUbm+RqvVg5i2iBM8EyijZNoI9wsOoOc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23 h1:9Fjh6fi/U5JEStVZijmaMpUwE/gvBJj7x2B/PjbO9To=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23/go.mod h1:iMoT2f1tClxrWAAnKCXjZQ6LOmfLrMG14wmnWpM+F14=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 h1:/Z5jmNrKsSD7EmDjzAPsm/3L9IuOkzaynklJZ1qX7S4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30/go.mod h1:lEzEZnOosE7zi8Z6royW1cFJTD9fpab4Ul1SBrllewk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31 h1:uao4A3QZ5UmB326V6KF+qRpv9Tjz7IlnlnTbbANntlU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31/go.mod h1:I/1+z0VwL1GhQyLgkoHDlygpUZ+iTAwOQ/NsftiUL2I=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0 h1:XptwLL+UHXgafYMIHTy59IRovLbhz3znkxY2uS/pbXU=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0/go.mod h1:zdmCoFO/dSI7GlrwsPqFJI+WlFnSU4Tc8TJnlXrM1Do=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.4.0 h1:sLzmJGCMv+C8KqiJgEqDLB6vxaJGmobRh4rr//ZpA3w=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.4.0/go.mod h1:mxC0nT/C8wMMS97DemZPzvUZxvIt+2Iq+eS3JdFZGgg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.32.0 h1:qjMmry/cBDee1E/2gyvel0uRYCi3mwRZ2hf6N+GAodo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.32.0/go.mod h1:u8af9Nqkmqnr96f7v9nHqzZT9XBwbXEkTiqT4ROuJSE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.0 h1:fpOlDPI55HdszaxapEGk6HsGosOUaM2YPWJpjMgp8UI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.0/go.mod h1:DMPWJBjYs6+3+f/qhBFEFPPlQ6NlhWjai3dJNvipJ84=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.44.0 h1:bLZ0PolJ8J+HkJHztcXORUpHXBye2U8298lCEMi6ZCU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.44.0/go.mod h1:9gdl4RrflIdpDb2TlXshWgR1F9TeCkvqDx77Vpr4Z/Q=
|
||||
github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY=
|
||||
github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.47.1 h1:uOIZnp4PK3ZhKI0dNrJrhTEsLxbpXHTAJlwoS1pvAtw=
|
||||
github.com/aws/aws-sdk-go-v2 v1.47.1/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 h1:GPRlPwz40I2B2VrBEASOA3Bi77NyeqejNLkifosX0rs=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20/go.mod h1:g7PNzKcsOKWb4fkSRBA7BZVAS6Y8IcxzN+nRohhQ1Q8=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.33.6 h1:MBjkSTLczek/UgiK+EYPIoRTqE7gP8vtW3OFbFo7Nug=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.33.6/go.mod h1:grRAFzdAZJrwcbasJRg2MPvIrVjtlfXllHssN6+E1JE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.6 h1:NpAFXCU7NzXNkdGK3zQTtsRJ+3v9tZQV0xcdRw8uBdw=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.6/go.mod h1:mcZCoiPnyMvP8VMNbygNX5lLqSlkYJIMPODylQMurOk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.1 h1:8gALAAmacnIXh+z6VkdDanv4/IkG5APdg4DZLDTmLog=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.1/go.mod h1:Z7IJhJU+poOdJjUR2wpyY21ossQ1XS/R3Lk9Msq5kM4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.4 h1:CLq4+8UHCI+ZZYl/EuJxXovaIVN2xeeT8JV+dsApQ5E=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.4/go.mod h1:Wv4q5sAM04xAMkoOedxLx2inVf6K5FdxYp+A61L+q/0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.4 h1:dD4MR81I7YkpEBRk6UP9rocC2QnT3qVuXwzlYTtfGEs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.4/go.mod h1:EcXV1kAFd5XwSkDHlj94gnF3q5CkJyYiIJfH8N0VmrE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.4 h1:7Wo47d/xn/7KttCSBd8EGYeZ7ULRFRkUHr6vkZPBzVQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.4/go.mod h1:tDB2IVC1xC3vX8o+6uRlzhTxP3g1b77CZXFX/oD2FnQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 h1:bAdDl/HkGCcGPoe25ToSHEw23VIxt6CT5fLcg111BKg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19/go.mod h1:KaUzbLxv4CeSxh6ZCl9B4m7CuFenS8kUEaDs+f/DQr4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.5 h1:/TYsZXdA8UTa+WCtCYSAJIr1vwl0+eho6TUgJGwFFO8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.5/go.mod h1:qPqp1Uwd/BqdhPufv6oem9j5J7HNsgc2V22dUiDPn+s=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.4 h1:29SvnfGhXjTl8ONxFwbj2rs6lbhiFXD2CgFQmbT/bXY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.4/go.mod h1:wm04I5DMuNVvZHFe/dHnUxincvNbbK7AiNBbYsQivek=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.4 h1:pPiWfgeNxqluKEph7hvU88kuGKBPOWzO+Dk9t2zqqNs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.4/go.mod h1:YlwGoIUDG/3kBQbdNOVs/xKZ9J01G8e/6D1mRBj9uTk=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.0 h1:VMAdYqr4Jn/8ATs9BHC5riwrs0d6m1Z2ohFriSwZwm0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.0/go.mod h1:9APRWGLFITKD+xzWSIyT9V7QV4bNlEuIieWlzXgGFlI=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.10.1 h1:DzCCWLzcIRQ77F3DEUljud7bEjTgFOIKXP52NmVRyhU=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.10.1/go.mod h1:xpo/geVldu8payT375WekctUzopG/hBU7miiqItMUlw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.38.1 h1:Umtl/0YZhng4xndfW3lKJrYYP7NLEjI6bGXVomwLcs0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.38.1/go.mod h1:rRD/dnm7q0HYE/I5TMaPgkWyyUGLcwuxHLABsLnQ3e0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.1 h1:orIWdNiLgzrhu/11RcPPKO/SBzUUymbUQuZbSPImghg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.1/go.mod h1:skwM/xsbR/1ReUTesv9BhpJp1VjajR7DWQnuVLwiXsQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.51.1 h1:0HOqZXRvMytH6bFHVIc0oJX07sZjfhz0zXtjs6gdE8s=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.51.1/go.mod h1:26zA0GhDrLo+yiLI2yXWxqB1PdsShfLikoI7GOEgugM=
|
||||
github.com/aws/smithy-go v1.28.2 h1:myhcykQcatTul2B/zITjDk203G7t0awUAs1hVry5Bvg=
|
||||
github.com/aws/smithy-go v1.28.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
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/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v7 v7.0.1/go.mod h1:qcKBGwsu4hpxHtQ8tWYsQ+ifzx2+sS+Xx/3jfe30lI8=
|
||||
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/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cloudsoda/go-smb2 v0.0.0-20260918041005-0c5d69b69701 h1:xiTow1n7oNJPzn9QH+U2sT65mO2F12zNNlY4ZzOew70=
|
||||
github.com/cloudsoda/go-smb2 v0.0.0-20260918041005-0c5d69b69701/go.mod h1:1pQXB0vAlzRlqcY7LYKOOZMw0wKfJPFxTLsJRF2Gswo=
|
||||
github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc h1:0xCWmFKBmarCqqqLeM7jFBSw/Or81UEElFqO8MY+GDs=
|
||||
github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc/go.mod h1:uvR42Hb/t52HQd7x5/ZLzZEK8oihrFpgnodIJ1vte2E=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/log v0.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc=
|
||||
github.com/containerd/log v0.2.0/go.mod h1:/M7L7CXKcPTfNC74XzaK+5H5KbO5+4lJVpuVI6vRLoM=
|
||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
|
||||
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/ebitengine/purego v0.11.1 h1:2zpWRSQNVKN4eKsKO9eM1ILDgWfYMY9GwqRmK6XeQ/0=
|
||||
github.com/ebitengine/purego v0.11.1/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
|
||||
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
|
||||
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
|
||||
github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=
|
||||
github.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6 h1:VtX+l1Stj2v5RGubVQk0LS/8EPGXR+ldcOyCmlmKoyg=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6/go.mod h1:PZpedQc4tWaxn6kvXicwhinh3L0seLpMc5ReKRX5id4=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.7 h1:PdT4jVPbRb4R+0Ey2R0yJOdctVf4Whiq1Qi4necaZdg=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.7/go.mod h1:3ouXglTuPrKF5+7cQyVGfvAXTU4vLMaYh9+EPl03uog=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
github.com/go-logr/logr v1.4.4/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/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||
github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME=
|
||||
github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gofiber/fiber/v2 v2.52.14 h1:Of3L+9qVFaQNwPlcmEdl5IIodHz8BSE0j37R7rWu4pE=
|
||||
github.com/gofiber/fiber/v2 v2.52.14/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/goccy/go-json v0.11.2 h1:jdZv93Tt4ioR8yW1CoNsvSxrcZlCXAUU1aZXN7gpXUA=
|
||||
github.com/goccy/go-json v0.11.2/go.mod h1:3NdmfEkZlB7YI5UFw/qdFKq8XN1aiWR0YyRPWZNQltY=
|
||||
github.com/gofiber/fiber/v2 v2.52.15 h1:Cov1uKeVPyu9q0jSrN60W+A8XNX+/WK8J7cy5osHLIk=
|
||||
github.com/gofiber/fiber/v2 v2.52.15/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/fiber/v3 v3.5.0/go.mod h1:GOVDTW+gjJvfe0iJyVujbQ1Lnx+JUjFySJRI/9/xX/w=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 h1:kEISI/Gx67NzH3nJxAmY/dGac80kKZgZt134u7Y/k1s=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4/go.mod h1:6Nz966r3vQYCqIzWsuEl9d7cf7mRhtDmm++sOxlnfxI=
|
||||
github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
|
||||
github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.4 h1:ZrN80XjMzpRYk+2FxMDy2A2zz0d5QjJ7GMFSkZLj12A=
|
||||
github.com/hashicorp/go-uuid v1.0.4/go.mod h1:x2Ds7vSkQ2n/yQj8Synnxmt0zt1l26uCAjxIhChisLU=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
|
||||
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
|
||||
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
|
||||
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
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=
|
||||
@@ -141,38 +157,44 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 h1:eveIIGn4BGM3qknO74omf6HYr30/exH+eVUTuAgwjZ0=
|
||||
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.18.12 h1:sT9zQpvTB3B4gzrX0tmZNTEaGyg8Zw55MFYRE32Mr9I=
|
||||
github.com/magiconair/properties v1.18.12/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
|
||||
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo=
|
||||
github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
|
||||
github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||
github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U=
|
||||
github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
|
||||
github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
|
||||
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
|
||||
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
|
||||
github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ=
|
||||
github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
|
||||
github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs=
|
||||
github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ=
|
||||
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00=
|
||||
github.com/moby/sys/mount v0.3.5/go.mod h1:WUQDO+/uCiCIkIztx8SrwIDVn2dtMFRBebRhpDFT71M=
|
||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
||||
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
|
||||
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
|
||||
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
|
||||
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
|
||||
github.com/moby/sys/userns v0.2.1 h1:4OvdM7BcPkASbuouHsbW3aeMJSFlYDldBRnXVZhaRk8=
|
||||
github.com/moby/sys/userns v0.2.1/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
|
||||
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
|
||||
github.com/molecule-man/go-brrr v1.1.1 h1:KYwusQhjtV3smALnhmafZf4OBrh7vP4p8O47DyUmmuY=
|
||||
github.com/molecule-man/go-brrr v1.1.1/go.mod h1:7ybW6/7gA3oKY45jOfVNjSJDtrr6ea4tzbsTkjmQDC4=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
@@ -181,20 +203,17 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI=
|
||||
github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY=
|
||||
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
|
||||
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
|
||||
github.com/power-devops/perfstat v0.0.0-20260916203055-22a1a467d9f0 h1:XA01Vk/wv9YikCi1V51yRzIHPMT5of9+cMpZoZvDn/M=
|
||||
github.com/power-devops/perfstat v0.0.0-20260916203055-22a1a467d9f0/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
|
||||
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||
@@ -202,10 +221,10 @@ github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/shirou/gopsutil/v4 v4.26.9 h1:CaBo/hBFqvlJoLvAQEKdHdf5GD97/MN2ACfyv63+2Ig=
|
||||
github.com/shirou/gopsutil/v4 v4.26.9/go.mod h1:nKH+8wX2zxr/mDbsR+AAy6Qd2z4ZOQ7wfQaEp+yrYmg=
|
||||
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
@@ -218,104 +237,122 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
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/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
|
||||
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
|
||||
github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0 h1:P9Txfy5Jothx2wFdcus0QoSmX/PKSIXZxrTbZPVJswA=
|
||||
github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0/go.mod h1:oZPHHqJqXG7FD8OB/yWH7gLnDvZUlFHAVJNrGftL+eg=
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 h1:s2bIayFXlbDFexo96y+htn7FzuhpXLYJNnIuglNKqOk=
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0/go.mod h1:h+u/2KoREGTnTl9UwrQ/g+XhasAT8E6dClclAADeXoQ=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/testcontainers/testcontainers-go v0.44.0 h1:/Fwh6HY1mIikhnm9e7HwoxGycx0lzRAE0f5VQpjFxzI=
|
||||
github.com/testcontainers/testcontainers-go v0.44.0/go.mod h1:IcnwQrYTO86xHXu5bvMaBH7ATlbS3Qn1M1QWW3c66rE=
|
||||
github.com/testcontainers/testcontainers-go/modules/mysql v0.44.0 h1:oJPJPxNE6YQ0zlq6mZKh06JOlyimCky4ruQUimdDet4=
|
||||
github.com/testcontainers/testcontainers-go/modules/mysql v0.44.0/go.mod h1:MSOAU6ukCpehJVHQDN1k9JgOZXZuqHD+2pT20M3JkIg=
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.44.0 h1:8fdv/9y3JMxjQ+ULAcOG8RtgeNu5t9XF9LolSXDuTwM=
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.44.0/go.mod h1:CFr2LncGYokw+OKjXcr8ARCKG1SaC2UEnGxFBovE86g=
|
||||
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
|
||||
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
|
||||
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
|
||||
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.72.0 h1:R7kYdoWhn1ye1fVpP+cDHDJwYm3NkwLliwgzJ/Abg7M=
|
||||
github.com/valyala/fasthttp v1.72.0/go.mod h1:zsbLTYqcpIktdQytlVBwIjY9La5d6bs990nBxWg8efk=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/valyala/fasthttp v1.74.0 h1:wMS9fnO2QTALozYx5pId2Vi7ZwU/epUkY8i/KPWCHoU=
|
||||
github.com/valyala/fasthttp v1.74.0/go.mod h1:3ARmLamUcw7ElxVtC8PXaGzQ6VEuvnetlkrwIklQBSE=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
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=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU=
|
||||
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
|
||||
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
|
||||
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
|
||||
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
|
||||
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
|
||||
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
|
||||
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
|
||||
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
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.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
|
||||
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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=
|
||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/driver/postgres v1.6.3 h1:bAn6O2pUa8LtpWEvL5NFU4+52Tfx8Ut7IVaIacCLcI0=
|
||||
gorm.io/driver/postgres v1.6.3/go.mod h1:0c4fQA44XhOklXDkgtuKqysHCycTa5i9e3EIpDGCwXk=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
|
||||
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
|
||||
+93
-31
@@ -5,10 +5,12 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -44,6 +46,7 @@ import (
|
||||
// App represents the main application
|
||||
type App struct {
|
||||
config *config.Config
|
||||
cancel context.CancelFunc
|
||||
app *fiber.App
|
||||
healthChecker *health.Checker
|
||||
cache *cache.Manager
|
||||
@@ -580,20 +583,13 @@ func (a *App) setupServer() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run starts the application
|
||||
func (a *App) Run() error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Start WebSocket server
|
||||
a.wsServer.Start(ctx)
|
||||
|
||||
// Start pre-warming worker
|
||||
a.prewarmWorker.Start(ctx)
|
||||
|
||||
// Start rescan worker if enabled
|
||||
if a.rescanWorker != nil {
|
||||
go a.rescanWorker.Start(ctx)
|
||||
}
|
||||
// Cancellable worker context: aggregation + stats broadcaster goroutines
|
||||
// watch ctx.Done() and exit, so Shutdown can stop them before closing
|
||||
// the storage/metadata backends they touch.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
a.cancel = cancel
|
||||
defer cancel()
|
||||
|
||||
// Start download data aggregation worker (runs every hour)
|
||||
go a.startAggregationWorker(ctx)
|
||||
@@ -605,7 +601,7 @@ func (a *App) Run() error {
|
||||
// Start Fiber server in goroutine. Branch on TLS to pick Listen vs ListenTLS.
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
addr := fmt.Sprintf("%s:%d", a.config.Server.Host, a.config.Server.Port)
|
||||
addr := net.JoinHostPort(a.config.Server.Host, strconv.Itoa(a.config.Server.Port))
|
||||
if a.config.Server.TLS.Enabled {
|
||||
log.Info().
|
||||
Str("addr", addr).
|
||||
@@ -630,6 +626,13 @@ func (a *App) Run() error {
|
||||
|
||||
select {
|
||||
case err := <-errChan:
|
||||
// Server failed to start or stopped unexpectedly. Cancel the worker
|
||||
// contexts and run the same teardown Shutdown performs so background
|
||||
// goroutines don't keep running against closed storage/metadata.
|
||||
shutdownErr := a.Shutdown()
|
||||
if shutdownErr != nil {
|
||||
log.Error().Err(shutdownErr).Msg("Error during shutdown after server error")
|
||||
}
|
||||
return fmt.Errorf("server error: %w", err)
|
||||
case sig := <-sigChan:
|
||||
log.Info().
|
||||
@@ -640,7 +643,6 @@ func (a *App) Run() error {
|
||||
// Graceful shutdown
|
||||
return a.Shutdown()
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the application
|
||||
func (a *App) Shutdown() error {
|
||||
log.Info().Msg("Starting graceful shutdown")
|
||||
@@ -650,6 +652,12 @@ func (a *App) Shutdown() error {
|
||||
log.Error().Err(err).Msg("Error shutting down Fiber server")
|
||||
}
|
||||
|
||||
// Cancel worker contexts first so the aggregation and stats broadcaster
|
||||
// goroutines stop before the storage/metadata backends they touch close.
|
||||
if a.cancel != nil {
|
||||
a.cancel()
|
||||
}
|
||||
|
||||
// Drain async auth writes (LastUsedAt updates) before closing metadata.
|
||||
if a.authManager != nil {
|
||||
a.authManager.Close()
|
||||
@@ -666,14 +674,15 @@ func (a *App) Shutdown() error {
|
||||
// Close analytics engine
|
||||
a.analyticsEngine.Close() // #nosec G104 -- Cleanup, error not critical
|
||||
|
||||
// Close storage
|
||||
if err := a.storage.Close(); err != nil {
|
||||
log.Error().Err(err).Msg("Error closing storage")
|
||||
}
|
||||
|
||||
// Close metadata store
|
||||
if err := a.metadata.Close(); err != nil {
|
||||
log.Error().Err(err).Msg("Error closing metadata store")
|
||||
// Close cache manager. This stops the cache cleanup worker and closes the
|
||||
// storage + metadata backends (a.cache is the sole owner of both, sharing
|
||||
// the same pointers with the App). Calling Close on storage/metadata here
|
||||
// instead of via a.cache.Close() would double-close and leave the cleanup
|
||||
// worker running against closed backends.
|
||||
if a.cache != nil {
|
||||
if err := a.cache.Close(); err != nil {
|
||||
log.Error().Err(err).Msg("Error closing cache")
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Msg("Shutdown complete")
|
||||
@@ -762,6 +771,41 @@ func getOrDefaultStr(value, defaultValue string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
// normalizeOriginHost lowercases a host and strips the scheme's default port
|
||||
// (443 for https, 80 for http) so "https://app.example.com:443" and
|
||||
// "https://app.example.com" are treated as the same origin. Non-default ports
|
||||
// are preserved. Returns ("", false) for an empty/invalid host.
|
||||
func normalizeOriginHost(scheme, host string) (string, bool) {
|
||||
host = strings.ToLower(strings.TrimSpace(host))
|
||||
if host == "" {
|
||||
return "", false
|
||||
}
|
||||
// Split off a port, if present. url.Parse stores the full host; use
|
||||
// net.SplitHostPort so IPv6 literals like [::1]:8080 are handled.
|
||||
h := host
|
||||
var port string
|
||||
if hh, p, err := net.SplitHostPort(host); err == nil {
|
||||
h = hh
|
||||
port = p
|
||||
}
|
||||
defaultPort := ""
|
||||
switch scheme {
|
||||
case "https":
|
||||
defaultPort = "443"
|
||||
case "http":
|
||||
defaultPort = "80"
|
||||
}
|
||||
if port != "" && port == defaultPort {
|
||||
return h, true
|
||||
}
|
||||
// Rebuild the host with the port (when present) for exact comparisons.
|
||||
if port != "" {
|
||||
// Reuse the original port form; net.JoinHostPort preserves IPv6 brackets.
|
||||
return net.JoinHostPort(h, port), true
|
||||
}
|
||||
return h, true
|
||||
}
|
||||
|
||||
// buildCheckOrigin returns a websocket Origin validator.
|
||||
//
|
||||
// Behaviour:
|
||||
@@ -771,13 +815,18 @@ func getOrDefaultStr(value, defaultValue string) string {
|
||||
// Origin host must match the Host header of the incoming request.
|
||||
// - requests with no Origin header are accepted (non-browser clients).
|
||||
//
|
||||
// Scheme-default ports (443/80) are normalized out of both the allowlist and
|
||||
// the incoming Origin, so "https://app.example.com" and
|
||||
// "https://app.example.com:443" are treated identically. Wildcard matching is
|
||||
// performed on the hostname only.
|
||||
//
|
||||
// We deliberately do NOT default to allow-all to avoid Cross-Site WebSocket
|
||||
// Hijacking (CSWSH).
|
||||
func buildCheckOrigin(allowed []string) func(*http.Request) bool {
|
||||
// Pre-parse allowlist once.
|
||||
type originRule struct {
|
||||
scheme string
|
||||
host string // exact host, or "" if hostSuffix is set
|
||||
host string // exact normalized host:port (default port stripped), or "" if hostGlob is set
|
||||
hostGlob string // suffix match including leading "."
|
||||
}
|
||||
rules := make([]originRule, 0, len(allowed))
|
||||
@@ -791,13 +840,18 @@ func buildCheckOrigin(allowed []string) func(*http.Request) bool {
|
||||
log.Warn().Str("entry", raw).Msg("Ignoring invalid AllowedOrigins entry")
|
||||
continue
|
||||
}
|
||||
r := originRule{scheme: strings.ToLower(u.Scheme)}
|
||||
host := strings.ToLower(u.Host)
|
||||
if strings.HasPrefix(host, "*.") {
|
||||
scheme := strings.ToLower(u.Scheme)
|
||||
normHost, ok := normalizeOriginHost(scheme, u.Host)
|
||||
if !ok {
|
||||
log.Warn().Str("entry", raw).Msg("Ignoring invalid AllowedOrigins entry")
|
||||
continue
|
||||
}
|
||||
r := originRule{scheme: scheme}
|
||||
if strings.HasPrefix(normHost, "*.") {
|
||||
// "*.example.com" → match any subdomain plus the apex.
|
||||
r.hostGlob = host[1:] // ".example.com"
|
||||
r.hostGlob = normHost[1:] // ".example.com"
|
||||
} else {
|
||||
r.host = host
|
||||
r.host = normHost
|
||||
}
|
||||
rules = append(rules, r)
|
||||
}
|
||||
@@ -814,11 +868,19 @@ func buildCheckOrigin(allowed []string) func(*http.Request) bool {
|
||||
return false
|
||||
}
|
||||
originScheme := strings.ToLower(ou.Scheme)
|
||||
originHost := strings.ToLower(ou.Host)
|
||||
originHost, ok := normalizeOriginHost(originScheme, ou.Host)
|
||||
if !ok {
|
||||
log.Warn().Str("origin", origin).Msg("Rejecting WebSocket upgrade: malformed Origin header")
|
||||
return false
|
||||
}
|
||||
|
||||
// Empty allowlist → same-origin only.
|
||||
if len(rules) == 0 {
|
||||
return strings.EqualFold(originHost, r.Host)
|
||||
reqHost, ok := normalizeOriginHost(originScheme, r.Host)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return originHost == reqHost
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ func (a *App) handleListPackages(c *fiber.Ctx) error {
|
||||
// Count vulnerabilities by severity
|
||||
severityCounts := make(map[string]int)
|
||||
for _, vuln := range scanResult.Vulnerabilities {
|
||||
severityCounts[strings.ToUpper(vuln.Severity)]++
|
||||
severityCounts[metadata.NormalizeSeverity(vuln.Severity)]++
|
||||
}
|
||||
|
||||
// Check if package should be blocked based on thresholds
|
||||
|
||||
@@ -1,36 +1,28 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/auth"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/errors"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/metadata"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// requireAdmin middleware checks for admin authentication
|
||||
// requireAdmin middleware checks for admin authentication. It accepts the API
|
||||
// key via either the Authorization Bearer header or the X-API-Key header
|
||||
// (shared extractAPIKey), so admin clients are not limited to the bearer form.
|
||||
func (a *App) requireAdmin(c *fiber.Ctx) error {
|
||||
// Get API key from Authorization header
|
||||
authHeader := c.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
// Get API key from Authorization or X-API-Key header
|
||||
apiKey, ok := extractAPIKey(c)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "missing authorization header",
|
||||
"error": "missing API key; provide Authorization: Bearer <key> or X-API-Key header",
|
||||
})
|
||||
}
|
||||
|
||||
// Extract bearer token
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "invalid authorization header format, expected: Bearer <token>",
|
||||
})
|
||||
}
|
||||
|
||||
apiKey := parts[1]
|
||||
|
||||
// Validate API key
|
||||
key, err := a.authManager.ValidateAPIKey(c.Context(), apiKey)
|
||||
if err != nil {
|
||||
@@ -304,7 +296,7 @@ func (a *App) handleDeleteBypass(c *fiber.Ctx) error {
|
||||
|
||||
// Delete bypass
|
||||
if err := a.metadata.DeleteCVEBypass(ctx, bypassID); err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if appErr, ok := err.(*errors.Error); ok && appErr.Code == errors.ErrCodeNotFound {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "bypass not found"})
|
||||
}
|
||||
log.Error().Err(err).Msg("Failed to delete bypass")
|
||||
|
||||
@@ -77,11 +77,19 @@ func (a *App) handleVulnerabilities(c *fiber.Ctx) error {
|
||||
bypasses = []*metadata.CVEBypass{}
|
||||
}
|
||||
|
||||
// Build bypass map for fast lookup
|
||||
// Build bypass map for fast lookup.
|
||||
// Normalize Go cache-key names (modulePath/@v/version.zip) to the
|
||||
// natural module path so admin bypass targets like
|
||||
// "go/github.com/foo/[email protected]" match the package.
|
||||
bypassName := name
|
||||
if registry == "go" {
|
||||
if idx := strings.Index(bypassName, "/@v/"); idx >= 0 {
|
||||
bypassName = bypassName[:idx]
|
||||
}
|
||||
}
|
||||
packageKey := registry + "/" + bypassName + "@" + version
|
||||
packageKeyNoVersion := registry + "/" + bypassName
|
||||
bypassedCVEs := make(map[string]*metadata.CVEBypass)
|
||||
packageKey := registry + "/" + name + "@" + version
|
||||
packageKeyNoVersion := registry + "/" + name
|
||||
|
||||
for _, bypass := range bypasses {
|
||||
if bypass.Type == metadata.BypassTypeCVE && bypass.Active {
|
||||
// Check if bypass applies to this package
|
||||
@@ -110,7 +118,7 @@ func (a *App) handleVulnerabilities(c *fiber.Ctx) error {
|
||||
}
|
||||
} else {
|
||||
// Count non-bypassed vulnerabilities by severity
|
||||
severityCounts[strings.ToUpper(vuln.Severity)]++
|
||||
severityCounts[metadata.NormalizeSeverity(vuln.Severity)]++
|
||||
}
|
||||
|
||||
enrichedVuln := map[string]interface{}{
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func reqWithOrigin(origin string) *http.Request {
|
||||
r, _ := http.NewRequest(http.MethodGet, "/ws", nil)
|
||||
if origin != "" {
|
||||
r.Header.Set("Origin", origin)
|
||||
}
|
||||
r.Host = "app.example.com"
|
||||
return r
|
||||
}
|
||||
|
||||
// TestBuildCheckOrigin_DefaultPortNormalization verifies that scheme-default
|
||||
// ports (443/80) are ignored, so "https://app.example.com" and
|
||||
// "https://app.example.com:443" are treated as the same origin.
|
||||
func TestBuildCheckOrigin_DefaultPortNormalization(t *testing.T) {
|
||||
check := buildCheckOrigin([]string{"https://app.example.com"})
|
||||
|
||||
assert.True(t, check(reqWithOrigin("https://app.example.com")))
|
||||
assert.True(t, check(reqWithOrigin("https://app.example.com:443")))
|
||||
assert.False(t, check(reqWithOrigin("https://app.example.com:8443")))
|
||||
|
||||
// Default http port strips too.
|
||||
checkHTTP := buildCheckOrigin([]string{"http://app.example.com"})
|
||||
assert.True(t, checkHTTP(reqWithOrigin("http://app.example.com:80")))
|
||||
assert.True(t, checkHTTP(reqWithOrigin("http://app.example.com")))
|
||||
}
|
||||
|
||||
// TestBuildCheckOrigin_NonDefaultPortPreserved verifies a non-default port is
|
||||
// not stripped and must match exactly.
|
||||
func TestBuildCheckOrigin_NonDefaultPortPreserved(t *testing.T) {
|
||||
check := buildCheckOrigin([]string{"https://app.example.com:8443"})
|
||||
|
||||
assert.True(t, check(reqWithOrigin("https://app.example.com:8443")))
|
||||
assert.False(t, check(reqWithOrigin("https://app.example.com")))
|
||||
}
|
||||
|
||||
// TestBuildCheckOrigin_Wildcard verifies wildcard host patterns match the apex
|
||||
// and subdomains, and are unaffected by ports on the incoming origin.
|
||||
func TestBuildCheckOrigin_Wildcard(t *testing.T) {
|
||||
check := buildCheckOrigin([]string{"https://*.example.com"})
|
||||
|
||||
assert.True(t, check(reqWithOrigin("https://example.com")))
|
||||
assert.True(t, check(reqWithOrigin("https://cdn.example.com")))
|
||||
assert.True(t, check(reqWithOrigin("https://cdn.example.com:443")))
|
||||
assert.False(t, check(reqWithOrigin("https://example.org")))
|
||||
}
|
||||
|
||||
// TestBuildCheckOrigin_SchemeMismatch verifies the scheme must match.
|
||||
func TestBuildCheckOrigin_SchemeMismatch(t *testing.T) {
|
||||
check := buildCheckOrigin([]string{"https://app.example.com"})
|
||||
|
||||
assert.False(t, check(reqWithOrigin("http://app.example.com")))
|
||||
}
|
||||
|
||||
// TestBuildCheckOrigin_SameOriginFallback verifies that with an empty allowlist
|
||||
// only a same-origin upgrade (matching Host) is permitted.
|
||||
func TestBuildCheckOrigin_SameOriginFallback(t *testing.T) {
|
||||
check := buildCheckOrigin(nil)
|
||||
|
||||
r := reqWithOrigin("https://app.example.com")
|
||||
assert.True(t, check(r))
|
||||
|
||||
// Host defaults to app.example.com; a different Origin host is rejected.
|
||||
r2 := reqWithOrigin("https://other.example.com")
|
||||
assert.False(t, check(r2))
|
||||
}
|
||||
@@ -12,7 +12,17 @@ type ValidationResult struct {
|
||||
Allowed bool
|
||||
}
|
||||
|
||||
// ValidationCache caches credential validation results to reduce upstream checks
|
||||
// ValidationCache caches credential validation results to reduce upstream checks.
|
||||
//
|
||||
// Scope: each instance is owned by exactly one proxy handler (goproxy, npm,
|
||||
// or pypi) and caches only that registry's credential checks. Because the
|
||||
// instances are never shared across handlers, the second key component may be
|
||||
// whatever uniquely identifies the resource for that registry: goproxy passes
|
||||
// a module path, npm and pypi pass a URL. There is no cross-registry key
|
||||
// collision even though the resource shapes differ, because each handler has
|
||||
// its own map. Callers must pass the same resource value to every Get/Set for
|
||||
// a given resource so the cache miss wrapper (Get then Set) is internally
|
||||
// consistent.
|
||||
type ValidationCache struct {
|
||||
cache map[string]*ValidationResult
|
||||
stopCh chan struct{}
|
||||
@@ -32,22 +42,15 @@ func NewValidationCache(ttl time.Duration) *ValidationCache {
|
||||
|
||||
// Start cleanup goroutine
|
||||
go vc.cleanupExpired()
|
||||
|
||||
return vc
|
||||
}
|
||||
|
||||
// Stop terminates the background cleanup goroutine. Safe to call multiple
|
||||
// times; subsequent calls are no-ops.
|
||||
func (vc *ValidationCache) Stop() {
|
||||
vc.stopOnce.Do(func() {
|
||||
close(vc.stopCh)
|
||||
})
|
||||
}
|
||||
|
||||
// Get retrieves a validation result from cache
|
||||
// Returns (allowed bool, cached bool, reason string)
|
||||
func (vc *ValidationCache) Get(credHash, packageURL string) (bool, bool, string) {
|
||||
key := credHash + ":" + packageURL
|
||||
// Get retrieves a validation result from cache.
|
||||
// resource names the registry resource the check was for (a module path for
|
||||
// goproxy, or a URL for npm/pypi). It is only meaningful within this cache
|
||||
// instance. Returns (allowed bool, cached bool, reason string).
|
||||
func (vc *ValidationCache) Get(credHash, resource string) (bool, bool, string) {
|
||||
key := credHash + ":" + resource
|
||||
|
||||
// Fast path: read-locked lookup.
|
||||
vc.mu.RLock()
|
||||
@@ -74,12 +77,14 @@ func (vc *ValidationCache) Get(credHash, packageURL string) (bool, bool, string)
|
||||
return false, false, ""
|
||||
}
|
||||
|
||||
// Set stores a validation result in cache
|
||||
func (vc *ValidationCache) Set(credHash, packageURL string, allowed bool, reason string) {
|
||||
// Set stores a validation result in cache.
|
||||
// resource is the registry resource the result belongs to; it must match the
|
||||
// resource value passed to the corresponding Get.
|
||||
func (vc *ValidationCache) Set(credHash, resource string, allowed bool, reason string) {
|
||||
vc.mu.Lock()
|
||||
defer vc.mu.Unlock()
|
||||
|
||||
key := credHash + ":" + packageURL
|
||||
key := credHash + ":" + resource
|
||||
vc.cache[key] = &ValidationResult{
|
||||
Allowed: allowed,
|
||||
ExpiresAt: time.Now().Add(vc.ttl),
|
||||
@@ -87,12 +92,12 @@ func (vc *ValidationCache) Set(credHash, packageURL string, allowed bool, reason
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate removes a specific entry from cache
|
||||
func (vc *ValidationCache) Invalidate(credHash, packageURL string) {
|
||||
// Invalidate removes a specific entry from cache.
|
||||
func (vc *ValidationCache) Invalidate(credHash, resource string) {
|
||||
vc.mu.Lock()
|
||||
defer vc.mu.Unlock()
|
||||
|
||||
key := credHash + ":" + packageURL
|
||||
key := credHash + ":" + resource
|
||||
delete(vc.cache, key)
|
||||
}
|
||||
|
||||
@@ -112,6 +117,14 @@ func (vc *ValidationCache) Size() int {
|
||||
return len(vc.cache)
|
||||
}
|
||||
|
||||
// Stop terminates the background cleanup goroutine. Safe to call multiple
|
||||
// times; subsequent calls are no-ops.
|
||||
func (vc *ValidationCache) Stop() {
|
||||
vc.stopOnce.Do(func() {
|
||||
close(vc.stopCh)
|
||||
})
|
||||
}
|
||||
|
||||
// cleanupExpired removes expired entries periodically. Exits when Stop is
|
||||
// called.
|
||||
func (vc *ValidationCache) cleanupExpired() {
|
||||
|
||||
@@ -18,6 +18,9 @@ type CredentialValidator interface {
|
||||
// ValidateAccess checks if credentials grant access to a package
|
||||
// Returns (allowed bool, error)
|
||||
ValidateAccess(ctx context.Context, packageURL string, credentials string) (bool, error)
|
||||
// Provider returns the upstream auth provider identifier stored on
|
||||
// packages that require auth (e.g. github, gitlab, npm, pypi).
|
||||
Provider() string
|
||||
}
|
||||
|
||||
// NPMValidator validates npm registry credentials
|
||||
@@ -36,6 +39,11 @@ func NewNPMValidator() *NPMValidator {
|
||||
}
|
||||
}
|
||||
|
||||
// Provider identifies this validator's upstream provider.
|
||||
func (v *NPMValidator) Provider() string {
|
||||
return "npm"
|
||||
}
|
||||
|
||||
// ValidateAccess validates npm package access using HEAD request
|
||||
func (v *NPMValidator) ValidateAccess(ctx context.Context, packageURL string, credentials string) (bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", packageURL, nil)
|
||||
@@ -87,6 +95,11 @@ func NewPyPIValidator() *PyPIValidator {
|
||||
}
|
||||
}
|
||||
|
||||
// Provider identifies this validator's upstream provider.
|
||||
func (v *PyPIValidator) Provider() string {
|
||||
return "pypi"
|
||||
}
|
||||
|
||||
// ValidateAccess validates PyPI package access using HEAD request
|
||||
func (v *PyPIValidator) ValidateAccess(ctx context.Context, packageURL string, credentials string) (bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", packageURL, nil)
|
||||
@@ -134,6 +147,26 @@ func NewGoValidator() *GoValidator {
|
||||
}
|
||||
}
|
||||
|
||||
// Provider returns the base upstream provider identifier used when the
|
||||
// registry does not distinguish hosts (e.g. a generic git host).
|
||||
func (v *GoValidator) Provider() string {
|
||||
return "git"
|
||||
}
|
||||
|
||||
// ProviderFor derives the specific provider identifier from a module path.
|
||||
// Mirrors the host branching in ValidateAccess so the stored AuthProvider
|
||||
// reflects the actual upstream host (github, gitlab, or a generic git host).
|
||||
func (v *GoValidator) ProviderFor(modulePath string) string {
|
||||
switch {
|
||||
case strings.HasPrefix(modulePath, "github.com/"):
|
||||
return "github"
|
||||
case strings.HasPrefix(modulePath, "gitlab.com/"):
|
||||
return "gitlab"
|
||||
default:
|
||||
return "git"
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateAccess validates Go module access using git ls-remote
|
||||
func (v *GoValidator) ValidateAccess(ctx context.Context, modulePath string, credentials string) (bool, error) {
|
||||
// Create context with timeout
|
||||
|
||||
Vendored
+98
-53
@@ -3,7 +3,6 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
@@ -96,6 +95,27 @@ type CacheEntry struct {
|
||||
FromCache bool
|
||||
}
|
||||
|
||||
// FetchResult carries the outcome of an upstream fetch closure. In addition
|
||||
// to the response body, it communicates auth state so the cache manager can
|
||||
// persist RequiresAuth/AuthProvider on the stored package. The downstream
|
||||
// security gate reads entry.Package.RequiresAuth, so this state must be stored
|
||||
// in the database (not just set on the ephemeral cache entry).
|
||||
type FetchResult struct {
|
||||
// Data is the response body. Caller closes it (or Get closes it after store).
|
||||
Data io.ReadCloser
|
||||
// UpstreamURL is the upstream URL the body came from.
|
||||
UpstreamURL string
|
||||
// AuthProvider identifies the auth system (e.g. github, gitlab, custom).
|
||||
AuthProvider string
|
||||
// RequiresAuth is true when the upstream indicated the package is behind
|
||||
// authentication (e.g. HTTP 401/403, or a private registry that demanded
|
||||
// credentials).
|
||||
RequiresAuth bool
|
||||
}
|
||||
|
||||
// FetchFunc fetches a package from upstream and reports auth state.
|
||||
type FetchFunc func(context.Context) (*FetchResult, error)
|
||||
|
||||
// New creates a new cache manager
|
||||
func New(storage storage.StorageBackend, metadata metadata.MetadataStore, scanner ScannerInterface, analytics AnalyticsInterface, config Config) (*Manager, error) {
|
||||
if storage == nil {
|
||||
@@ -150,10 +170,10 @@ func New(storage storage.StorageBackend, metadata metadata.MetadataStore, scanne
|
||||
go manager.cleanupWorker()
|
||||
|
||||
return manager, nil
|
||||
|
||||
}
|
||||
|
||||
// Get retrieves a package from cache or upstream
|
||||
func (m *Manager) Get(ctx context.Context, registry, name, version string, fetchFunc func(context.Context) (io.ReadCloser, string, error)) (*CacheEntry, error) {
|
||||
func (m *Manager) Get(ctx context.Context, registry, name, version string, fetchFunc FetchFunc) (*CacheEntry, error) {
|
||||
// Use singleflight to deduplicate concurrent requests
|
||||
key := fmt.Sprintf("%s/%s/%s", registry, name, version)
|
||||
|
||||
@@ -190,7 +210,7 @@ func (m *Manager) Get(ctx context.Context, registry, name, version string, fetch
|
||||
}
|
||||
|
||||
// getOrFetch implements the actual get-or-fetch logic
|
||||
func (m *Manager) getOrFetch(ctx context.Context, registry, name, version string, fetchFunc func(context.Context) (io.ReadCloser, string, error)) (*CacheEntry, error) {
|
||||
func (m *Manager) getOrFetch(ctx context.Context, registry, name, version string, fetchFunc FetchFunc) (*CacheEntry, error) {
|
||||
// Check metadata first
|
||||
pkg, err := m.metadata.GetPackage(ctx, registry, name, version)
|
||||
if err == nil {
|
||||
@@ -247,8 +267,13 @@ func (m *Manager) getOrFetch(ctx context.Context, registry, name, version string
|
||||
m.trackDownload(registry, name, version, pkg.Size)
|
||||
}
|
||||
|
||||
// Check for vulnerabilities if scanner is enabled
|
||||
if m.scanner != nil {
|
||||
// Check for vulnerabilities if scanner is enabled. Metadata
|
||||
// entries (index pages, lists, .mod/.info) are not scannable
|
||||
// packages, so skip them to avoid a bogus "clean" verdict and
|
||||
// the extra database round-trip.
|
||||
isMetadataEntry := version == "list" || version == "page" || version == "latest" || version == "metadata" ||
|
||||
strings.HasSuffix(name, ".mod") || strings.HasSuffix(name, ".info")
|
||||
if m.scanner != nil && !isMetadataEntry {
|
||||
blocked, reason, vulnErr := m.scanner.CheckVulnerabilities(ctx, registry, name, version)
|
||||
if vulnErr != nil {
|
||||
log.Warn().Err(vulnErr).Str("package", name).Msg("Failed to check vulnerabilities")
|
||||
@@ -293,17 +318,22 @@ func (m *Manager) getOrFetch(ctx context.Context, registry, name, version string
|
||||
log.Debug().Str("package", name).Str("version", version).Msg("Fetching from upstream")
|
||||
|
||||
// Fetch from upstream
|
||||
data, upstreamURL, err := fetchFunc(ctx)
|
||||
fr, err := fetchFunc(ctx)
|
||||
if err != nil {
|
||||
metrics.RecordUpstreamRequest(registry, "error")
|
||||
return nil, errors.Wrap(err, errors.ErrCodeUpstreamFailure, "failed to fetch from upstream")
|
||||
}
|
||||
if fr == nil || fr.Data == nil {
|
||||
return nil, errors.New(errors.ErrCodeUpstreamFailure, "upstream fetch returned no data")
|
||||
}
|
||||
data := fr.Data
|
||||
defer func() { _ = data.Close() }() // #nosec G104 -- Cleanup, error not critical
|
||||
|
||||
metrics.RecordUpstreamRequest(registry, "success")
|
||||
|
||||
// Store in cache (this will also trigger background scan)
|
||||
storedPkg, err := m.store(ctx, registry, name, version, data, upstreamURL)
|
||||
// Store in cache (this will also trigger background scan). Auth state is
|
||||
// plumbed through so the persisted package carries RequiresAuth.
|
||||
storedPkg, err := m.store(ctx, registry, name, version, data, fr.UpstreamURL, fr.RequiresAuth, fr.AuthProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -412,45 +442,56 @@ func (m *Manager) getOrFetch(ctx context.Context, registry, name, version string
|
||||
// Data is intentionally nil; Get() opens a fresh reader per caller.
|
||||
return &CacheEntry{
|
||||
Package: storedPkg,
|
||||
Data: nil,
|
||||
UpstreamURL: fr.UpstreamURL,
|
||||
FromCache: false,
|
||||
UpstreamURL: upstreamURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// store stores a package in cache
|
||||
func (m *Manager) store(ctx context.Context, registry, name, version string, data io.ReadCloser, upstreamURL string) (*metadata.Package, error) {
|
||||
func (m *Manager) store(ctx context.Context, registry, name, version string, data io.ReadCloser, upstreamURL string, requiresAuth bool, authProvider string) (*metadata.Package, error) {
|
||||
// Generate storage key
|
||||
storageKey := m.generateStorageKey(registry, name, version)
|
||||
|
||||
// Calculate checksums while storing
|
||||
// We need to read the data, calculate checksums, and store it
|
||||
// This requires buffering the data
|
||||
var buf []byte
|
||||
var err error
|
||||
|
||||
// Cap upstream read to MaxPackageSize to prevent OOM on hostile/oversized
|
||||
// upstream responses. Read one extra byte so we can detect overflow.
|
||||
// Stream upstream to a temp file while computing checksums, instead of
|
||||
// buffering the full payload in memory (avoids OOM on large packages,
|
||||
// e.g. the default 2 GB MaxPackageSize). The temp file also becomes the
|
||||
// scan source for backends that can't expose a local path, so the payload
|
||||
// is never held twice in RAM.
|
||||
maxSize := m.config.MaxPackageSize
|
||||
if maxSize <= 0 {
|
||||
maxSize = 2 * 1024 * 1024 * 1024 // 2GB safety floor
|
||||
}
|
||||
limited := io.LimitReader(data, maxSize+1)
|
||||
buf, err = io.ReadAll(limited)
|
||||
|
||||
tempFile, err := os.CreateTemp(os.TempDir(), "gohoarder-store-*")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, errors.ErrCodeUpstreamFailure, "failed to create temp file for upstream data")
|
||||
}
|
||||
tempPath := tempFile.Name()
|
||||
// Temp file is removed when store() returns, unless ownership is handed
|
||||
// to a scan goroutine (remote backends reuse it for scanning).
|
||||
scanOwnsTemp := false
|
||||
defer func() {
|
||||
if !scanOwnsTemp {
|
||||
_ = os.Remove(tempPath) // #nosec G104 -- cleanup
|
||||
}
|
||||
}()
|
||||
|
||||
// Compute SHA256 as the payload is streamed to the temp file. Cap at
|
||||
// maxSize+1 so we can detect overflow without reading unbounded data.
|
||||
hash := sha256.New()
|
||||
limited := io.LimitReader(data, maxSize+1)
|
||||
written, err := io.Copy(io.MultiWriter(tempFile, hash), limited)
|
||||
if err != nil {
|
||||
_ = tempFile.Close() // #nosec G104 -- cleanup
|
||||
return nil, errors.Wrap(err, errors.ErrCodeUpstreamFailure, "failed to read upstream data")
|
||||
}
|
||||
if int64(len(buf)) > maxSize {
|
||||
if written > maxSize {
|
||||
_ = tempFile.Close() // #nosec G104 -- cleanup
|
||||
return nil, errors.New(errors.ErrCodePayloadTooLarge,
|
||||
fmt.Sprintf("upstream package exceeds max size (%d bytes)", maxSize))
|
||||
}
|
||||
|
||||
// Calculate checksums
|
||||
h := sha256.New()
|
||||
h.Write(buf)
|
||||
checksumSHA256 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
size := int64(len(buf))
|
||||
checksumSHA256 := fmt.Sprintf("%x", hash.Sum(nil))
|
||||
size := written
|
||||
|
||||
// Check quota before storing
|
||||
quota, err := m.storage.GetQuota(ctx)
|
||||
@@ -458,20 +499,28 @@ func (m *Manager) store(ctx context.Context, registry, name, version string, dat
|
||||
if quota.Used+size > quota.Limit {
|
||||
// Trigger eviction
|
||||
if evictErr := m.evict(ctx, size); evictErr != nil {
|
||||
_ = tempFile.Close() // #nosec G104 -- cleanup
|
||||
return nil, errors.QuotaExceeded(quota.Limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Seek temp file back to start and stream it to the storage backend.
|
||||
if _, err := tempFile.Seek(0, io.SeekStart); err != nil {
|
||||
_ = tempFile.Close() // #nosec G104 -- cleanup
|
||||
return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to seek temp file")
|
||||
}
|
||||
|
||||
// Store in storage backend
|
||||
opts := &storage.PutOptions{
|
||||
ChecksumSHA256: checksumSHA256,
|
||||
}
|
||||
|
||||
err = m.storage.Put(ctx, storageKey, io.NopCloser(bytes.NewReader(buf)), opts)
|
||||
if err != nil {
|
||||
if err := m.storage.Put(ctx, storageKey, tempFile, opts); err != nil {
|
||||
_ = tempFile.Close() // #nosec G104 -- cleanup
|
||||
return nil, err
|
||||
}
|
||||
_ = tempFile.Close() // #nosec G104 -- cleanup; scan may still read the file
|
||||
|
||||
// Create metadata entry
|
||||
now := time.Now()
|
||||
@@ -486,6 +535,8 @@ func (m *Manager) store(ctx context.Context, registry, name, version string, dat
|
||||
Size: size,
|
||||
ChecksumSHA256: checksumSHA256,
|
||||
UpstreamURL: upstreamURL,
|
||||
RequiresAuth: requiresAuth,
|
||||
AuthProvider: authProvider,
|
||||
CachedAt: now,
|
||||
LastAccessed: now,
|
||||
ExpiresAt: &expiresAt,
|
||||
@@ -531,6 +582,16 @@ func (m *Manager) store(ctx context.Context, registry, name, version string, dat
|
||||
// Scan package if scanner is enabled (run in background to not block cache operations)
|
||||
// Skip scanning metadata entries (index pages, lists, etc.)
|
||||
if m.scanner != nil && !isMetadataEntry {
|
||||
// Decide synchronously whether the streaming temp file must outlive
|
||||
// store(). Remote backends (no GetLocalPath) reuse it as the scan
|
||||
// source; local backends scan from GetLocalPath and don't need it.
|
||||
_, hasLocalPath := m.storage.(interface {
|
||||
GetLocalPath(ctx context.Context, key string) (string, error)
|
||||
})
|
||||
if !hasLocalPath {
|
||||
scanOwnsTemp = true
|
||||
}
|
||||
|
||||
go func() {
|
||||
scanCtx := context.Background()
|
||||
var filePath string
|
||||
@@ -550,27 +611,11 @@ func (m *Manager) store(ctx context.Context, registry, name, version string, dat
|
||||
cleanupFunc = func() {} // No cleanup needed for direct path
|
||||
log.Debug().Str("package", name).Str("path", filePath).Msg("Scanning package from storage path")
|
||||
} else {
|
||||
// Fallback: Create temp file for remote storage (S3, SMB, etc.).
|
||||
// Use os.CreateTemp so the OS picks a safe, unique filename — this
|
||||
// prevents path traversal via storageKey containing "..".
|
||||
tempFile, err := os.CreateTemp(os.TempDir(), "gohoarder-scan-*")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("package", name).Msg("Failed to create temp file for scanning")
|
||||
return
|
||||
}
|
||||
tempFilePath := tempFile.Name()
|
||||
|
||||
// Write package data to temp file
|
||||
if _, err := tempFile.Write(buf); err != nil {
|
||||
_ = tempFile.Close() // #nosec G104 -- Cleanup, error not critical
|
||||
_ = os.Remove(tempFilePath) // #nosec G104 -- Cleanup, error not critical
|
||||
log.Error().Err(err).Str("package", name).Msg("Failed to write temp file for scanning")
|
||||
return
|
||||
}
|
||||
_ = tempFile.Close() // #nosec G104 -- Cleanup, error not critical
|
||||
|
||||
filePath = tempFilePath
|
||||
cleanupFunc = func() { _ = os.Remove(tempFilePath) } // #nosec G104 -- Cleanup
|
||||
// Remote storage (S3, SMB, etc.) — reuse the temp file store()
|
||||
// already wrote during streaming. This avoids re-uploading the
|
||||
// payload into another temp file and avoids holding it in RAM.
|
||||
filePath = tempPath
|
||||
cleanupFunc = func() { _ = os.Remove(tempPath) } // #nosec G104 -- Cleanup
|
||||
log.Debug().Str("package", name).Str("path", filePath).Msg("Scanning package from temp file")
|
||||
}
|
||||
|
||||
|
||||
Vendored
+19
-19
@@ -355,7 +355,7 @@ func TestNew(t *testing.T) {
|
||||
func TestGet(t *testing.T) {
|
||||
tests := []struct {
|
||||
setupMock func(*MockStorageBackend, *MockMetadataStore)
|
||||
fetchFunc func(context.Context) (io.ReadCloser, string, error)
|
||||
fetchFunc FetchFunc
|
||||
name string
|
||||
registry string
|
||||
packageName string
|
||||
@@ -404,8 +404,8 @@ func TestGet(t *testing.T) {
|
||||
s.On("Get", mock.Anything, "npm/lodash/4.17.21").Return(io.NopCloser(strings.NewReader("upstream data")), nil)
|
||||
m.On("UpdateDownloadCount", mock.Anything, "npm", "lodash", "4.17.21").Return(nil)
|
||||
},
|
||||
fetchFunc: func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
return io.NopCloser(strings.NewReader("upstream data")), "https://registry.npmjs.org/lodash", nil
|
||||
fetchFunc: func(ctx context.Context) (*FetchResult, error) {
|
||||
return &FetchResult{Data: io.NopCloser(strings.NewReader("upstream data")), UpstreamURL: "https://registry.npmjs.org/lodash"}, nil
|
||||
},
|
||||
wantFromCache: false,
|
||||
wantErr: false,
|
||||
@@ -436,8 +436,8 @@ func TestGet(t *testing.T) {
|
||||
s.On("Get", mock.Anything, "npm/expired-pkg/1.0.0").Return(io.NopCloser(strings.NewReader("refreshed data")), nil)
|
||||
m.On("UpdateDownloadCount", mock.Anything, "npm", "expired-pkg", "1.0.0").Return(nil)
|
||||
},
|
||||
fetchFunc: func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
return io.NopCloser(strings.NewReader("refreshed data")), "https://registry.npmjs.org/expired-pkg", nil
|
||||
fetchFunc: func(ctx context.Context) (*FetchResult, error) {
|
||||
return &FetchResult{Data: io.NopCloser(strings.NewReader("refreshed data")), UpstreamURL: "https://registry.npmjs.org/expired-pkg"}, nil
|
||||
},
|
||||
wantFromCache: false,
|
||||
wantErr: false,
|
||||
@@ -464,8 +464,8 @@ func TestGet(t *testing.T) {
|
||||
setupMock: func(s *MockStorageBackend, m *MockMetadataStore) {
|
||||
m.On("GetPackage", mock.Anything, "npm", "fail-pkg", "1.0.0").Return(nil, errors.New("not found"))
|
||||
},
|
||||
fetchFunc: func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
return nil, "", errors.New("upstream error")
|
||||
fetchFunc: func(ctx context.Context) (*FetchResult, error) {
|
||||
return nil, errors.New("upstream error")
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "failed to fetch from upstream",
|
||||
@@ -498,8 +498,8 @@ func TestGet(t *testing.T) {
|
||||
s.On("Get", mock.Anything, "npm/inconsistent/1.0.0").Return(io.NopCloser(strings.NewReader("recovered data")), nil).Once()
|
||||
m.On("UpdateDownloadCount", mock.Anything, "npm", "inconsistent", "1.0.0").Return(nil)
|
||||
},
|
||||
fetchFunc: func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
return io.NopCloser(strings.NewReader("recovered data")), "https://registry.npmjs.org/inconsistent", nil
|
||||
fetchFunc: func(ctx context.Context) (*FetchResult, error) {
|
||||
return &FetchResult{Data: io.NopCloser(strings.NewReader("recovered data")), UpstreamURL: "https://registry.npmjs.org/inconsistent"}, nil
|
||||
},
|
||||
wantFromCache: false,
|
||||
wantErr: false,
|
||||
@@ -515,8 +515,8 @@ func TestGet(t *testing.T) {
|
||||
s.On("GetQuota", mock.Anything).Return(&storage.QuotaInfo{Used: 100, Available: 900, Limit: 1000}, nil)
|
||||
s.On("Put", mock.Anything, "npm/save-fail/1.0.0", mock.Anything, mock.Anything).Return(errors.New("storage error"))
|
||||
},
|
||||
fetchFunc: func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
return io.NopCloser(strings.NewReader("data")), "https://registry.npmjs.org/save-fail", nil
|
||||
fetchFunc: func(ctx context.Context) (*FetchResult, error) {
|
||||
return &FetchResult{Data: io.NopCloser(strings.NewReader("data")), UpstreamURL: "https://registry.npmjs.org/save-fail"}, nil
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "storage error",
|
||||
@@ -534,8 +534,8 @@ func TestGet(t *testing.T) {
|
||||
m.On("SavePackage", mock.Anything, mock.Anything).Return(errors.New("metadata error"))
|
||||
s.On("Delete", mock.Anything, "npm/meta-fail/1.0.0").Return(nil)
|
||||
},
|
||||
fetchFunc: func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
return io.NopCloser(strings.NewReader("data")), "https://registry.npmjs.org/meta-fail", nil
|
||||
fetchFunc: func(ctx context.Context) (*FetchResult, error) {
|
||||
return &FetchResult{Data: io.NopCloser(strings.NewReader("data")), UpstreamURL: "https://registry.npmjs.org/meta-fail"}, nil
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "metadata error",
|
||||
@@ -1105,8 +1105,8 @@ func TestBroadcaster_CacheMissStore(t *testing.T) {
|
||||
bc := &fakeBroadcaster{}
|
||||
manager.SetBroadcaster(bc)
|
||||
|
||||
fetch := func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
return io.NopCloser(strings.NewReader("upstream data")), "https://registry.npmjs.org/lodash", nil
|
||||
fetch := func(ctx context.Context) (*FetchResult, error) {
|
||||
return &FetchResult{Data: io.NopCloser(strings.NewReader("upstream data")), UpstreamURL: "https://registry.npmjs.org/lodash"}, nil
|
||||
}
|
||||
entry, err := manager.Get(context.Background(), "npm", "lodash", "4.17.21", fetch)
|
||||
require.NoError(t, err)
|
||||
@@ -1153,8 +1153,8 @@ func TestBroadcaster_NoEmitOnError(t *testing.T) {
|
||||
bc := &fakeBroadcaster{}
|
||||
manager.SetBroadcaster(bc)
|
||||
|
||||
fetch := func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
return io.NopCloser(strings.NewReader("data")), "https://registry.npmjs.org/fail", nil
|
||||
fetch := func(ctx context.Context) (*FetchResult, error) {
|
||||
return &FetchResult{Data: io.NopCloser(strings.NewReader("data")), UpstreamURL: "https://registry.npmjs.org/fail"}, nil
|
||||
}
|
||||
_, err = manager.Get(context.Background(), "npm", "fail", "1.0.0", fetch)
|
||||
require.Error(t, err)
|
||||
@@ -1178,8 +1178,8 @@ func TestBroadcaster_NoEmitOnError(t *testing.T) {
|
||||
bc := &fakeBroadcaster{}
|
||||
manager.SetBroadcaster(bc)
|
||||
|
||||
fetch := func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
return io.NopCloser(strings.NewReader("data")), "https://registry.npmjs.org/meta-fail", nil
|
||||
fetch := func(ctx context.Context) (*FetchResult, error) {
|
||||
return &FetchResult{Data: io.NopCloser(strings.NewReader("data")), UpstreamURL: "https://registry.npmjs.org/meta-fail"}, nil
|
||||
}
|
||||
_, err = manager.Get(context.Background(), "npm", "meta-fail", "1.0.0", fetch)
|
||||
require.Error(t, err)
|
||||
|
||||
@@ -362,6 +362,16 @@ func Default() *Config {
|
||||
Security: SecurityConfig{
|
||||
Enabled: false,
|
||||
BlockOnSeverity: "high",
|
||||
// Default all severity caps to unlimited (-1) so the operator's
|
||||
// BlockOnSeverity decision governs by default. A zero cap means
|
||||
// "block any", which would make a single LOW vulnerability block
|
||||
// every package even though the configured default is "high".
|
||||
BlockThresholds: VulnerabilityThresholds{
|
||||
Critical: -1,
|
||||
High: -1,
|
||||
Medium: -1,
|
||||
Low: -1,
|
||||
},
|
||||
Scanners: ScannersConfig{
|
||||
Trivy: TrivyConfig{
|
||||
Enabled: false,
|
||||
|
||||
@@ -265,8 +265,22 @@ func (s *GORMStoreV2) SavePackage(ctx context.Context, pkg *metadata.Package) er
|
||||
return errors.Wrap(result.Error, errors.ErrCodeStorageFailure, "failed to update package")
|
||||
}
|
||||
|
||||
// If no rows were updated, create new record
|
||||
// If no rows were updated, create new record.
|
||||
// RowsAffected == 0 here means no LIVE row matches the key, because
|
||||
// GORM's default scope appends `deleted_at IS NULL` to the UPDATE.
|
||||
// The only possible match is a soft-deleted row (e.g. after TTL/evict
|
||||
// called DeletePackage). That retained row still holds the same
|
||||
// `storage_key`, which has a UNIQUE index, so a plain Create would
|
||||
// collide (UNIQUE constraint failed: packages.storage_key) and the
|
||||
// re-fetch would permanently 502. Hard-delete any such stale row
|
||||
// before creating.
|
||||
if result.RowsAffected == 0 {
|
||||
if err := tx.Unscoped().
|
||||
Where("registry_id = ? AND name = ? AND version = ?", registryID, pkg.Name, pkg.Version).
|
||||
Delete(&PackageModel{}).Error; err != nil {
|
||||
return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to purge stale soft-deleted package before create")
|
||||
}
|
||||
|
||||
if err := tx.Create(model).Error; err != nil {
|
||||
return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to create package")
|
||||
}
|
||||
|
||||
@@ -209,6 +209,57 @@ func (s *GORMStoreV2TestSuite) Test_V2_DeletePackage_Success() {
|
||||
s.Equal(int64(1), count) // Still in DB, just soft deleted
|
||||
}
|
||||
|
||||
// Test_V2_SavePackage_AfterSoftDelete tests that saving the same
|
||||
// registry/name/version key after a soft delete (e.g. TTL expiry or
|
||||
// cache eviction) succeeds instead of colliding on the retained
|
||||
// storage_key UNIQUE index. Regression for the permanent 502 on
|
||||
// re-fetch of a once-evicted package.
|
||||
func (s *GORMStoreV2TestSuite) Test_V2_SavePackage_AfterSoftDelete() {
|
||||
pkg := &metadata.Package{
|
||||
Registry: "npm",
|
||||
Name: "re-add-test",
|
||||
Version: "1.0.0",
|
||||
StorageKey: "npm/re-add-test/1.0.0.tgz",
|
||||
Size: 1000,
|
||||
CachedAt: time.Now(),
|
||||
LastAccessed: time.Now(),
|
||||
}
|
||||
err := s.store.SavePackage(s.ctx, pkg)
|
||||
s.NoError(err)
|
||||
|
||||
// Soft-delete (simulates TTL expiry / eviction)
|
||||
err = s.store.DeletePackage(s.ctx, "npm", "re-add-test", "1.0.0")
|
||||
s.NoError(err)
|
||||
|
||||
// Re-save the same key (simulates re-fetch after cache miss).
|
||||
// Before the fix this failed with:
|
||||
// UNIQUE constraint failed: packages.storage_key
|
||||
pkg2 := &metadata.Package{
|
||||
Registry: "npm",
|
||||
Name: "re-add-test",
|
||||
Version: "1.0.0",
|
||||
StorageKey: "npm/re-add-test/1.0.0.tgz",
|
||||
Size: 2000,
|
||||
CachedAt: time.Now(),
|
||||
LastAccessed: time.Now(),
|
||||
}
|
||||
err = s.store.SavePackage(s.ctx, pkg2)
|
||||
s.NoError(err)
|
||||
|
||||
// The re-saved package must be retrievable
|
||||
retrieved, err := s.store.GetPackage(s.ctx, "npm", "re-add-test", "1.0.0")
|
||||
s.NoError(err)
|
||||
s.NotNil(retrieved)
|
||||
s.Equal(int64(2000), retrieved.Size)
|
||||
|
||||
// No duplicate rows retained after the hard purge
|
||||
var count int64
|
||||
s.store.db.Unscoped().Model(&PackageModel{}).
|
||||
Where("name = ?", "re-add-test").
|
||||
Count(&count)
|
||||
s.Equal(int64(1), count)
|
||||
}
|
||||
|
||||
// Test_V2_ListPackages_All tests listing all packages
|
||||
func (s *GORMStoreV2TestSuite) Test_V2_ListPackages_All() {
|
||||
// Create multiple packages
|
||||
|
||||
@@ -229,19 +229,12 @@ func (c *Client) do(ctx context.Context, req *http.Request) (*http.Response, err
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
if c.circuitBreaker != nil {
|
||||
c.circuitBreaker.RecordFailure()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if response is retryable
|
||||
if c.isRetryable(resp.StatusCode) {
|
||||
_ = resp.Body.Close() // #nosec G104 -- Cleanup, error not critical
|
||||
lastErr = fmt.Errorf("received retryable status code: %d", resp.StatusCode)
|
||||
if c.circuitBreaker != nil {
|
||||
c.circuitBreaker.RecordFailure()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -405,3 +405,38 @@ func TestConcurrentRequests(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCircuitBreakerOpens verifies that a configured number of consecutive
|
||||
// failed requests opens the circuit breaker and that subsequent requests are
|
||||
// rejected with the circuit-open error until the breaker half-open window
|
||||
// elapses and a request succeeds again.
|
||||
func TestCircuitBreakerOpens(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close() // #nosec G104 -- Cleanup, error not critical
|
||||
|
||||
client := network.NewClient(network.Config{
|
||||
Timeout: 5 * time.Second,
|
||||
MaxRetries: 0,
|
||||
CircuitBreaker: network.CircuitBreakerConfig{
|
||||
Enabled: true,
|
||||
FailureThreshold: 3,
|
||||
SuccessThreshold: 1,
|
||||
Timeout: time.Hour,
|
||||
},
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Three consecutive failed requests open the breaker.
|
||||
for i := 0; i < 3; i++ {
|
||||
_, _, err := client.Get(ctx, server.URL, nil)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// The next request is rejected because the breaker is open.
|
||||
_, _, err := client.Get(ctx, server.URL, nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "circuit breaker is open")
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ func (h *Handler) handleList(ctx context.Context, w http.ResponseWriter, r *http
|
||||
// Extract credentials from request
|
||||
credentials := h.credExtractor.Extract(r)
|
||||
|
||||
entry, err := h.cache.Get(ctx, "go", modulePath, "list", func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
entry, err := h.cache.Get(ctx, "go", modulePath, "list", func(ctx context.Context) (*cache.FetchResult, error) {
|
||||
// Prepare headers for upstream request
|
||||
headers := make(map[string]string)
|
||||
if credentials != "" {
|
||||
@@ -124,13 +124,16 @@ func (h *Handler) handleList(ctx context.Context, w http.ResponseWriter, r *http
|
||||
|
||||
body, statusCode, err := h.client.Get(ctx, url, headers)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url, RequiresAuth: true, AuthProvider: h.credValidator.ProviderFor(modulePath)}, nil
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
_ = body.Close() // #nosec G104 -- Cleanup, error not critical
|
||||
return nil, "", fmt.Errorf("upstream returned status %d", statusCode)
|
||||
return nil, fmt.Errorf("upstream returned status %d", statusCode)
|
||||
}
|
||||
return body, url, nil
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url}, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -155,7 +158,7 @@ func (h *Handler) handleInfo(ctx context.Context, w http.ResponseWriter, r *http
|
||||
// Extract credentials from request
|
||||
credentials := h.credExtractor.Extract(r)
|
||||
|
||||
entry, err := h.cache.Get(ctx, "go", cacheKey, version, func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
entry, err := h.cache.Get(ctx, "go", cacheKey, version, func(ctx context.Context) (*cache.FetchResult, error) {
|
||||
// Prepare headers for upstream request
|
||||
headers := make(map[string]string)
|
||||
if credentials != "" {
|
||||
@@ -164,13 +167,16 @@ func (h *Handler) handleInfo(ctx context.Context, w http.ResponseWriter, r *http
|
||||
|
||||
body, statusCode, err := h.client.Get(ctx, url, headers)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url, RequiresAuth: true, AuthProvider: h.credValidator.ProviderFor(modulePath)}, nil
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
_ = body.Close() // #nosec G104 -- Cleanup, error not critical
|
||||
return nil, "", fmt.Errorf("upstream returned status %d", statusCode)
|
||||
return nil, fmt.Errorf("upstream returned status %d", statusCode)
|
||||
}
|
||||
return body, url, nil
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url}, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -188,14 +194,14 @@ func (h *Handler) handleInfo(ctx context.Context, w http.ResponseWriter, r *http
|
||||
func (h *Handler) handleMod(ctx context.Context, w http.ResponseWriter, r *http.Request, path string) {
|
||||
url := h.upstream + path
|
||||
modulePath := h.extractModulePath(path)
|
||||
|
||||
// Extract credentials from request
|
||||
credentials := h.credExtractor.Extract(r)
|
||||
version := h.extractVersion(path, ".mod")
|
||||
// Use .mod suffix to distinguish from .info and .zip in cache
|
||||
cacheKey := modulePath + "/@v/" + version + ".mod"
|
||||
|
||||
// Extract credentials from request
|
||||
credentials := h.credExtractor.Extract(r)
|
||||
|
||||
entry, err := h.cache.Get(ctx, "go", cacheKey, version, func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
entry, err := h.cache.Get(ctx, "go", cacheKey, version, func(ctx context.Context) (*cache.FetchResult, error) {
|
||||
// Prepare headers for upstream request
|
||||
headers := make(map[string]string)
|
||||
if credentials != "" {
|
||||
@@ -204,13 +210,16 @@ func (h *Handler) handleMod(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
|
||||
body, statusCode, err := h.client.Get(ctx, url, headers)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url, RequiresAuth: true, AuthProvider: h.credValidator.ProviderFor(modulePath)}, nil
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
_ = body.Close() // #nosec G104 -- Cleanup, error not critical
|
||||
return nil, "", fmt.Errorf("upstream returned status %d", statusCode)
|
||||
return nil, fmt.Errorf("upstream returned status %d", statusCode)
|
||||
}
|
||||
return body, url, nil
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url}, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -245,7 +254,7 @@ func (h *Handler) handleZip(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
Bool("has_credentials", credentials != "").
|
||||
Msg("Handling Go module zip request")
|
||||
|
||||
entry, err := h.cache.Get(ctx, "go", cacheKey, version, func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
entry, err := h.cache.Get(ctx, "go", cacheKey, version, func(ctx context.Context) (*cache.FetchResult, error) {
|
||||
// Prepare headers for upstream request
|
||||
headers := make(map[string]string)
|
||||
if credentials != "" {
|
||||
@@ -255,7 +264,7 @@ func (h *Handler) handleZip(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
// Try upstream proxy first (fast path for public modules)
|
||||
body, statusCode, err := h.client.Get(ctx, url, headers)
|
||||
if err == nil && statusCode == http.StatusOK {
|
||||
return body, url, nil
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url}, nil
|
||||
}
|
||||
|
||||
// If upstream failed with 404 or 403, try git fallback (private modules)
|
||||
@@ -278,9 +287,9 @@ func (h *Handler) handleZip(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
_ = body.Close() // #nosec G104 -- Cleanup, error not critical
|
||||
}
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
return nil, "", fmt.Errorf("upstream returned status %d", statusCode)
|
||||
return nil, fmt.Errorf("upstream returned status %d", statusCode)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -299,6 +308,10 @@ func (h *Handler) handleZip(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
defer func() { _ = entry.Data.Close() }() // #nosec G104 -- Cleanup, error not critical
|
||||
|
||||
// CRITICAL SECURITY CHECK: If module requires auth, validate credentials
|
||||
// The auth validation cache is owned by this handler instance only, so its
|
||||
// key may use the module path (modulePath) rather than a URL. The module path
|
||||
// uniquely identifies the private module within this registry's cache; there
|
||||
// is no cross-registry collision because npm/pypi use their own instances.
|
||||
if entry.Package != nil && entry.Package.RequiresAuth {
|
||||
// Check validation cache first
|
||||
allowed, cached, reason := h.validationCache.Get(credHash, modulePath)
|
||||
@@ -352,6 +365,7 @@ func (h *Handler) handleZip(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
_, _ = io.Copy(w, entry.Data) // #nosec G104 -- HTTP response write
|
||||
|
||||
}
|
||||
|
||||
// handleLatest handles /@latest requests
|
||||
@@ -362,7 +376,7 @@ func (h *Handler) handleLatest(ctx context.Context, w http.ResponseWriter, r *ht
|
||||
// Extract credentials from request
|
||||
credentials := h.credExtractor.Extract(r)
|
||||
|
||||
entry, err := h.cache.Get(ctx, "go", modulePath, "latest", func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
entry, err := h.cache.Get(ctx, "go", modulePath, "latest", func(ctx context.Context) (*cache.FetchResult, error) {
|
||||
// Prepare headers for upstream request
|
||||
headers := make(map[string]string)
|
||||
if credentials != "" {
|
||||
@@ -371,13 +385,16 @@ func (h *Handler) handleLatest(ctx context.Context, w http.ResponseWriter, r *ht
|
||||
|
||||
body, statusCode, err := h.client.Get(ctx, url, headers)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url, RequiresAuth: true, AuthProvider: h.credValidator.ProviderFor(modulePath)}, nil
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
_ = body.Close() // #nosec G104 -- Cleanup, error not critical
|
||||
return nil, "", fmt.Errorf("upstream returned status %d", statusCode)
|
||||
return nil, fmt.Errorf("upstream returned status %d", statusCode)
|
||||
}
|
||||
return body, url, nil
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url}, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -450,7 +467,7 @@ func (h *Handler) extractModulePath(path string) string {
|
||||
}
|
||||
|
||||
// fetchModuleFromGit fetches a Go module directly from git repository
|
||||
func (h *Handler) fetchModuleFromGit(ctx context.Context, modulePath, version, credentials string) (io.ReadCloser, string, error) {
|
||||
func (h *Handler) fetchModuleFromGit(ctx context.Context, modulePath, version, credentials string) (*cache.FetchResult, error) {
|
||||
log.Info().
|
||||
Str("module", modulePath).
|
||||
Str("version", version).
|
||||
@@ -459,23 +476,25 @@ func (h *Handler) fetchModuleFromGit(ctx context.Context, modulePath, version, c
|
||||
// 1. Fetch module source from git
|
||||
srcPath, err := h.gitFetcher.FetchModule(ctx, modulePath, version, credentials)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("git fetch failed: %w", err)
|
||||
return nil, fmt.Errorf("git fetch failed: %w", err)
|
||||
}
|
||||
defer h.gitFetcher.Cleanup(srcPath)
|
||||
|
||||
// 2. Validate module
|
||||
if validateErr := h.moduleBuilder.ValidateModule(ctx, srcPath, modulePath); validateErr != nil {
|
||||
return nil, "", fmt.Errorf("module validation failed: %w", validateErr)
|
||||
return nil, fmt.Errorf("module validation failed: %w", validateErr)
|
||||
}
|
||||
|
||||
// 3. Build module zip
|
||||
zipReader, err := h.moduleBuilder.BuildModuleZip(ctx, srcPath, modulePath, version)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("module zip build failed: %w", err)
|
||||
return nil, fmt.Errorf("module zip build failed: %w", err)
|
||||
}
|
||||
|
||||
// Create source URL for logging
|
||||
sourceURL := fmt.Sprintf("git+https://%s@%s", modulePath, version)
|
||||
// Create source URL for logging. Private modules fetched from git require
|
||||
// auth, so mark the result accordingly. FIX-L3: strip the git+ scheme so the
|
||||
// stored UpstreamURL is a fetchable HTTPS URL.
|
||||
sourceURL := fmt.Sprintf("https://%s@%s", modulePath, version)
|
||||
|
||||
log.Info().
|
||||
Str("module", modulePath).
|
||||
@@ -483,5 +502,10 @@ func (h *Handler) fetchModuleFromGit(ctx context.Context, modulePath, version, c
|
||||
Str("source", sourceURL).
|
||||
Msg("Successfully built module from git")
|
||||
|
||||
return zipReader, sourceURL, nil
|
||||
return &cache.FetchResult{
|
||||
Data: zipReader,
|
||||
UpstreamURL: sourceURL,
|
||||
RequiresAuth: true,
|
||||
AuthProvider: h.credValidator.ProviderFor(modulePath),
|
||||
}, nil
|
||||
}
|
||||
|
||||
+18
-10
@@ -80,16 +80,19 @@ func (h *Handler) handleMetadata(ctx context.Context, w http.ResponseWriter, r *
|
||||
url := h.upstream + path
|
||||
packageName := extractPackageName(path)
|
||||
|
||||
entry, err := h.cache.Get(ctx, "npm", packageName, "metadata", func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
entry, err := h.cache.Get(ctx, "npm", packageName, "metadata", func(ctx context.Context) (*cache.FetchResult, error) {
|
||||
body, statusCode, fetchErr := h.client.Get(ctx, url, nil)
|
||||
if fetchErr != nil {
|
||||
return nil, "", fetchErr
|
||||
return nil, fetchErr
|
||||
}
|
||||
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url, RequiresAuth: true, AuthProvider: h.credValidator.Provider()}, nil
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
_ = body.Close()
|
||||
return nil, "", fmt.Errorf("upstream returned status %d", statusCode)
|
||||
return nil, fmt.Errorf("upstream returned status %d", statusCode)
|
||||
}
|
||||
return body, url, nil
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url}, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -158,9 +161,7 @@ func (h *Handler) handleTarball(ctx context.Context, w http.ResponseWriter, r *h
|
||||
Str("cred_hash", credHash).
|
||||
Bool("has_credentials", credentials != "").
|
||||
Msg("Handling tarball request")
|
||||
|
||||
// Try to get from cache first (with credential-aware key)
|
||||
entry, err := h.cache.Get(ctx, "npm", packageName, version, func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
entry, err := h.cache.Get(ctx, "npm", packageName, version, func(ctx context.Context) (*cache.FetchResult, error) {
|
||||
// Prepare headers for upstream request
|
||||
headers := make(map[string]string)
|
||||
if credentials != "" {
|
||||
@@ -169,13 +170,16 @@ func (h *Handler) handleTarball(ctx context.Context, w http.ResponseWriter, r *h
|
||||
|
||||
body, statusCode, fetchErr := h.client.Get(ctx, url, headers)
|
||||
if fetchErr != nil {
|
||||
return nil, "", fetchErr
|
||||
return nil, fetchErr
|
||||
}
|
||||
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url, RequiresAuth: true, AuthProvider: h.credValidator.Provider()}, nil
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
_ = body.Close()
|
||||
return nil, "", fmt.Errorf("upstream returned status %d", statusCode)
|
||||
return nil, fmt.Errorf("upstream returned status %d", statusCode)
|
||||
}
|
||||
return body, url, nil
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url}, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -198,6 +202,10 @@ func (h *Handler) handleTarball(ctx context.Context, w http.ResponseWriter, r *h
|
||||
}()
|
||||
|
||||
// CRITICAL SECURITY CHECK: If package requires auth, validate credentials
|
||||
// The validation cache is owned by this handler instance only, so the key
|
||||
// uses the upstream tarball URL. Uniquely identifies the artifact within
|
||||
// this registry; no cross-registry collision (goproxy/pypi keep their own
|
||||
// independent instances).
|
||||
if entry.Package != nil && entry.Package.RequiresAuth {
|
||||
// Check validation cache first
|
||||
allowed, cached, reason := h.validationCache.Get(credHash, url)
|
||||
|
||||
+77
-41
@@ -124,16 +124,19 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) handleIndex(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
url := h.upstream + "/"
|
||||
|
||||
entry, err := h.cache.Get(ctx, "pypi", "index", "latest", func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
entry, err := h.cache.Get(ctx, "pypi", "index", "latest", func(ctx context.Context) (*cache.FetchResult, error) {
|
||||
body, statusCode, err := h.client.Get(ctx, url, nil)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url, RequiresAuth: true, AuthProvider: h.credValidator.Provider()}, nil
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
_ = body.Close()
|
||||
return nil, "", fmt.Errorf("upstream returned status %d", statusCode)
|
||||
return nil, fmt.Errorf("upstream returned status %d", statusCode)
|
||||
}
|
||||
return body, url, nil
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url}, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -155,17 +158,19 @@ func (h *Handler) handleIndex(ctx context.Context, w http.ResponseWriter, r *htt
|
||||
func (h *Handler) handlePackagePage(ctx context.Context, w http.ResponseWriter, r *http.Request, path string) {
|
||||
url := h.upstream + path
|
||||
packageName := extractPackageName(path)
|
||||
|
||||
entry, err := h.cache.Get(ctx, "pypi", packageName, "page", func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
entry, err := h.cache.Get(ctx, "pypi", packageName, "page", func(ctx context.Context) (*cache.FetchResult, error) {
|
||||
body, statusCode, err := h.client.Get(ctx, url, nil)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url, RequiresAuth: true, AuthProvider: h.credValidator.Provider()}, nil
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
_ = body.Close()
|
||||
return nil, "", fmt.Errorf("upstream returned status %d", statusCode)
|
||||
return nil, fmt.Errorf("upstream returned status %d", statusCode)
|
||||
}
|
||||
return body, url, nil
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: url}, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -199,16 +204,17 @@ func (h *Handler) handlePackagePage(ctx context.Context, w http.ResponseWriter,
|
||||
func (h *Handler) handlePackageFile(ctx context.Context, w http.ResponseWriter, r *http.Request, path string) {
|
||||
packageName, version := extractPackageFileInfo(path)
|
||||
|
||||
// Make version unique by appending file type to avoid cache collisions
|
||||
// between .whl and .metadata files with same version
|
||||
cacheVersion := version
|
||||
if strings.HasSuffix(path, ".metadata") {
|
||||
cacheVersion = version + ".metadata"
|
||||
} else if strings.HasSuffix(path, ".whl") {
|
||||
cacheVersion = version + ".whl"
|
||||
} else if strings.HasSuffix(path, ".tar.gz") {
|
||||
cacheVersion = version + ".tar.gz"
|
||||
// cacheVersion is the cache key's version portion. It must be unique per
|
||||
// distinct artifact. PyPI publishes many files per release (multiple
|
||||
// wheels with different build tags, sdists, zips, .egg files, and
|
||||
// .metadata files for PEP 658), so the bare version is too coarse:
|
||||
// distinct files of the same version would collide on the same cache
|
||||
// key. The full unique basename guarantees one cache entry per artifact.
|
||||
filename := path
|
||||
if i := strings.LastIndex(filename, "/"); i >= 0 {
|
||||
filename = filename[i+1:]
|
||||
}
|
||||
cacheVersion := filename
|
||||
|
||||
// Extract credentials from request
|
||||
credentials := h.credExtractor.Extract(r)
|
||||
@@ -255,7 +261,7 @@ func (h *Handler) handlePackageFile(ctx context.Context, w http.ResponseWriter,
|
||||
Bool("has_credentials", credentials != "").
|
||||
Msg("Handling PyPI package file request")
|
||||
|
||||
entry, err := h.cache.Get(ctx, "pypi", packageName, cacheVersion, func(ctx context.Context) (io.ReadCloser, string, error) {
|
||||
entry, err := h.cache.Get(ctx, "pypi", packageName, cacheVersion, func(ctx context.Context) (*cache.FetchResult, error) {
|
||||
// Prepare headers for upstream request
|
||||
headers := make(map[string]string)
|
||||
if credentials != "" {
|
||||
@@ -264,13 +270,16 @@ func (h *Handler) handlePackageFile(ctx context.Context, w http.ResponseWriter,
|
||||
|
||||
body, statusCode, err := h.client.Get(ctx, originalURL, headers)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: originalURL, RequiresAuth: true, AuthProvider: h.credValidator.Provider()}, nil
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
_ = body.Close()
|
||||
return nil, "", fmt.Errorf("upstream returned status %d", statusCode)
|
||||
return nil, fmt.Errorf("upstream returned status %d", statusCode)
|
||||
}
|
||||
return body, originalURL, nil
|
||||
return &cache.FetchResult{Data: body, UpstreamURL: originalURL}, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -293,6 +302,10 @@ func (h *Handler) handlePackageFile(ctx context.Context, w http.ResponseWriter,
|
||||
}()
|
||||
|
||||
// CRITICAL SECURITY CHECK: If package requires auth, validate credentials
|
||||
// The validation cache is owned by this handler instance only, so the key
|
||||
// uses the upstream artifact URL (originalURL). Uniquely identifies the
|
||||
// artifact within this registry; no cross-registry collision (goproxy/npm
|
||||
// keep their own independent instances).
|
||||
if entry.Package != nil && entry.Package.RequiresAuth {
|
||||
// Check validation cache first
|
||||
allowed, cached, reason := h.validationCache.Get(credHash, originalURL)
|
||||
@@ -395,37 +408,60 @@ func extractPackageName(path string) string {
|
||||
func extractPackageFileInfo(path string) (string, string) {
|
||||
// Format: /package-name/package-name-version.whl
|
||||
// or: /package-name/package-name-version.tar.gz
|
||||
|
||||
packageName := extractPackageName(path)
|
||||
|
||||
// Extract filename
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) < 2 {
|
||||
return packageName, ""
|
||||
}
|
||||
|
||||
filename := parts[len(parts)-1]
|
||||
base := parts[len(parts)-1]
|
||||
isWheel := strings.HasSuffix(base, ".whl")
|
||||
base = strings.TrimSuffix(base, ".whl")
|
||||
base = strings.TrimSuffix(base, ".tar.gz")
|
||||
base = strings.TrimSuffix(base, ".zip")
|
||||
base = strings.TrimSuffix(base, ".egg")
|
||||
base = strings.TrimSuffix(base, ".metadata")
|
||||
|
||||
// Remove extension
|
||||
filename = strings.TrimSuffix(filename, ".whl")
|
||||
filename = strings.TrimSuffix(filename, ".tar.gz")
|
||||
filename = strings.TrimSuffix(filename, ".zip")
|
||||
filename = strings.TrimSuffix(filename, ".egg")
|
||||
return packageName, extractVersionFromFilename(base, isWheel)
|
||||
}
|
||||
|
||||
// Extract version
|
||||
// Filename format: package-name-version or package_name-version
|
||||
// Version typically starts after last dash before build tags
|
||||
versionParts := strings.Split(filename, "-")
|
||||
if len(versionParts) >= 2 {
|
||||
// Simple heuristic: version is the part that starts with a digit
|
||||
for i := 1; i < len(versionParts); i++ {
|
||||
if len(versionParts[i]) > 0 && versionParts[i][0] >= '0' && versionParts[i][0] <= '9' {
|
||||
return packageName, versionParts[i]
|
||||
}
|
||||
// extractVersionFromFilename extracts the version from a PyPI distribution
|
||||
// filename with the extension already stripped.
|
||||
//
|
||||
// Wheels (PEP 427) are formatted as
|
||||
//
|
||||
// {distribution}-{version}(-{build})?-{python}-{abi}-{platform}
|
||||
//
|
||||
// and the distribution is normalized (lowercase, runs of '-', '.', '_'
|
||||
// collapsed to '_'), so it never contains '-' and the version is always the
|
||||
// second dash-separated segment. For sdists/zips/eggs the filename is
|
||||
// {name}-{version}; PyPI normalizes name separators to '_', so the version
|
||||
// is always the last dash-separated segment (PEP 440 versions never contain
|
||||
// '-').
|
||||
//
|
||||
// The historical first-digit-segment heuristic is avoided because it mangles
|
||||
// digit-embedded distribution names (e.g. "2captcha-1.0.0" → "2captcha") and
|
||||
// intermediate digit segments (e.g. "foo-2019bar-1.0" → "2019bar"). The last
|
||||
// digit-starting segment is correct for those cases.
|
||||
func extractVersionFromFilename(base string, isWheel bool) string {
|
||||
if isWheel {
|
||||
// The distribution never contains '-', so the version immediately
|
||||
// follows the first '-'.
|
||||
if parts := strings.SplitN(base, "-", 3); len(parts) >= 2 && parts[1] != "" && parts[1][0] >= '0' && parts[1][0] <= '9' {
|
||||
return parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
return packageName, filename
|
||||
// Non-wheel fallback (and defensive wheel edge case): the last
|
||||
// dash-separated segment that starts with a digit.
|
||||
parts := strings.Split(base, "-")
|
||||
for i := len(parts) - 1; i >= 1; i-- {
|
||||
if len(parts[i]) > 0 && parts[i][0] >= '0' && parts[i][0] <= '9' {
|
||||
return parts[i]
|
||||
}
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// getProxyBaseURL constructs the proxy base URL from the request
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/auth"
|
||||
@@ -137,3 +138,72 @@ func indexOf(s, sub string) int {
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func TestExtractPackageFileInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
wantName string
|
||||
wantVer string
|
||||
}{
|
||||
{"numpy wheel", "/numpy/numpy-1.26.0rc1-cp312-cp312-manylinux.whl", "numpy", "1.26.0rc1"},
|
||||
{"underscore wheel name", "/typing_extensions/typing_extensions-4.8.0-py3-none-any.whl", "typing_extensions", "4.8.0"},
|
||||
{"hyphenated sdist", "/my-pkg/my-pkg-2.0.1.tar.gz", "my-pkg", "2.0.1"},
|
||||
{"hyphen name underscore file", "/typed-ast/typed_ast-1.5.4.tar.gz", "typed-ast", "1.5.4"},
|
||||
{"digit-embedded name", "/2captcha/2captcha-1.0.0.tar.gz", "2captcha", "1.0.0"},
|
||||
{"digit intermediate segment", "/foo/foo-2019bar-1.0.zip", "foo", "1.0"},
|
||||
{"egg file", "/foo/foo-1.0.egg", "foo", "1.0"},
|
||||
{"metadata file", "/foo/foo-1.0.metadata", "foo", "1.0"},
|
||||
{"no version", "/foo", "foo", "foo"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotName, gotVer := extractPackageFileInfo(tt.path)
|
||||
if gotName != tt.wantName {
|
||||
t.Errorf("extractPackageFileInfo(%q) name = %q, want %q", tt.path, gotName, tt.wantName)
|
||||
}
|
||||
if gotVer != tt.wantVer {
|
||||
t.Errorf("extractPackageFileInfo(%q) version = %q, want %q", tt.path, gotVer, tt.wantVer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCacheVersionUniquePerArtifact asserts that distinct PyPI artifacts of
|
||||
// the same logical version produce distinct cache keys, and that the bare
|
||||
// version returned for logging stays stable.
|
||||
func TestCacheVersionUniquePerArtifact(t *testing.T) {
|
||||
// Three wheels of the same version differing only in build/tag, plus a
|
||||
// .zip and a .metadata file for the same version. The cache key uses the
|
||||
// full unique basename, so keys must all differ; the extracted version
|
||||
// must remain the bare stable version for logging/bypass logic.
|
||||
paths := []string{
|
||||
"/foo/foo-1.0.0-cp312-cp312-manylinux.whl",
|
||||
"/foo/foo-1.0.0-cp311-cp311-manylinux.whl",
|
||||
"/foo/foo-1.0.0-py3-none-any.whl",
|
||||
"/foo/foo-1.0.0.zip",
|
||||
"/foo/foo-1.0.0.metadata",
|
||||
}
|
||||
|
||||
seenKeys := make(map[string]bool)
|
||||
for _, path := range paths {
|
||||
_, ver := extractPackageFileInfo(path)
|
||||
if ver != "1.0.0" {
|
||||
t.Errorf("extractPackageFileInfo(%q) version = %q, want stable %q", path, ver, "1.0.0")
|
||||
}
|
||||
|
||||
filename := path
|
||||
if i := strings.LastIndex(filename, "/"); i >= 0 {
|
||||
filename = filename[i+1:]
|
||||
}
|
||||
if seenKeys[filename] {
|
||||
t.Errorf("cache key %q duplicated across artifacts", filename)
|
||||
}
|
||||
seenKeys[filename] = true
|
||||
}
|
||||
|
||||
if len(seenKeys) != len(paths) {
|
||||
t.Errorf("expected %d distinct cache keys, got %d", len(paths), len(seenKeys))
|
||||
}
|
||||
}
|
||||
|
||||
+20
-4
@@ -377,7 +377,7 @@ func (m *Manager) mergeResults(results []*metadata.ScanResult, scannerNames []st
|
||||
// Convert map to slice and count severities
|
||||
for _, vuln := range vulnMap {
|
||||
merged.Vulnerabilities = append(merged.Vulnerabilities, *vuln)
|
||||
severityCounts[strings.ToUpper(vuln.Severity)]++
|
||||
severityCounts[metadata.NormalizeSeverity(vuln.Severity)]++
|
||||
}
|
||||
|
||||
// Update counts
|
||||
@@ -409,6 +409,21 @@ func (m *Manager) compareSeverity(s1, s2 string) int {
|
||||
return v1 - v2
|
||||
}
|
||||
|
||||
// normalizeBypassPackageName reduces a stored package cache key to the
|
||||
// natural registry/package identifier used in admin bypass targets.
|
||||
// Go module packages are stored with a cache key of
|
||||
// "modulePath/@v/version.zip" (and .info/.mod variants); the admin-facing
|
||||
// target uses the bare module path, e.g. "go/github.com/foo/[email protected]".
|
||||
// npm/pypi names are stored unchanged, so this is a no-op for them.
|
||||
func normalizeBypassPackageName(registry, name string) string {
|
||||
if registry == "go" {
|
||||
if idx := strings.Index(name, "/@v/"); idx >= 0 {
|
||||
return name[:idx]
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// CheckVulnerabilities checks if a package exceeds vulnerability thresholds
|
||||
func (m *Manager) CheckVulnerabilities(ctx context.Context, registry, packageName, version string) (bool, string, error) {
|
||||
if !m.enabled {
|
||||
@@ -423,8 +438,9 @@ func (m *Manager) CheckVulnerabilities(ctx context.Context, registry, packageNam
|
||||
}
|
||||
|
||||
// Check if entire package is bypassed
|
||||
packageKey := fmt.Sprintf("%s/%s@%s", registry, packageName, version)
|
||||
packageKeyNoVersion := fmt.Sprintf("%s/%s", registry, packageName)
|
||||
bypassName := normalizeBypassPackageName(registry, packageName)
|
||||
packageKey := fmt.Sprintf("%s/%s@%s", registry, bypassName, version)
|
||||
packageKeyNoVersion := fmt.Sprintf("%s/%s", registry, bypassName)
|
||||
|
||||
for _, bypass := range bypasses {
|
||||
if bypass.Type == metadata.BypassTypePackage && bypass.Active {
|
||||
@@ -499,7 +515,7 @@ func (m *Manager) CheckVulnerabilities(ctx context.Context, registry, packageNam
|
||||
Msg("CVE bypassed by admin")
|
||||
continue
|
||||
}
|
||||
severityCounts[strings.ToUpper(vuln.Severity)]++
|
||||
severityCounts[metadata.NormalizeSeverity(vuln.Severity)]++
|
||||
}
|
||||
|
||||
// Check against thresholds
|
||||
|
||||
@@ -254,3 +254,79 @@ func TestBroadcaster_NilBroadcasterSafe(t *testing.T) {
|
||||
err := mgr.ScanPackage(context.Background(), "npm", "x", "1", "/tmp/dummy")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// stubBypassStore returns a package bypass and a clean scan result for a
|
||||
// Go cache-key package, to exercise FIX-H4 bypass-name normalization.
|
||||
type stubBypassStore struct {
|
||||
*stubMetadataStore
|
||||
scan *metadata.ScanResult
|
||||
bypasses []*metadata.CVEBypass
|
||||
}
|
||||
|
||||
func (m *stubBypassStore) GetActiveCVEBypasses(context.Context) ([]*metadata.CVEBypass, error) {
|
||||
return m.bypasses, nil
|
||||
}
|
||||
|
||||
func (m *stubBypassStore) GetScanResult(context.Context, string, string, string) (*metadata.ScanResult, error) {
|
||||
return m.scan, nil
|
||||
}
|
||||
|
||||
// TestCheckVulnerabilities_GoPackageBypassNaturalTarget verifies a package
|
||||
// bypass targeting the natural Go module path matches a package stored under
|
||||
// its cache-key name (modulePath/@v/version.zip).
|
||||
func TestCheckVulnerabilities_GoPackageBypassNaturalTarget(t *testing.T) {
|
||||
store := &stubBypassStore{
|
||||
stubMetadataStore: &stubMetadataStore{},
|
||||
bypasses: []*metadata.CVEBypass{
|
||||
{
|
||||
ID: "b1",
|
||||
Type: metadata.BypassTypePackage,
|
||||
Target: "go/github.com/foo/[email protected]",
|
||||
Active: true,
|
||||
},
|
||||
},
|
||||
scan: &metadata.ScanResult{
|
||||
ID: "r", Scanner: "stub", ScannedAt: time.Now(), Status: metadata.ScanStatusClean,
|
||||
},
|
||||
}
|
||||
mgr := newTestManager(t, store)
|
||||
|
||||
// The package is stored under the cache-key name; the version is passed
|
||||
// separately. The bypass target uses the natural module path.
|
||||
blocked, reason, err := mgr.CheckVulnerabilities(
|
||||
context.Background(), "go", "github.com/foo/bar/@v/v1.2.3.zip", "v1.2.3")
|
||||
require.NoError(t, err)
|
||||
assert.False(t, blocked, "package bypass should unblock the Go package")
|
||||
assert.Equal(t, "", reason)
|
||||
}
|
||||
|
||||
// TestCheckVulnerabilities_GoPackageBypassNotFound verifies a package bypass
|
||||
// targeting a different module does NOT unblock the Go package. Its stored
|
||||
// scan result is clean, so CheckVulnerabilities returns not-blocked anyway;
|
||||
// to make the package-block path observable we use a scan result with a
|
||||
// high-severity vulnerability and zero thresholds (block any non-zero).
|
||||
func TestCheckVulnerabilities_GoPackageBypassNotFound(t *testing.T) {
|
||||
store := &stubBypassStore{
|
||||
stubMetadataStore: &stubMetadataStore{},
|
||||
bypasses: []*metadata.CVEBypass{
|
||||
{
|
||||
ID: "b1",
|
||||
Type: metadata.BypassTypePackage,
|
||||
Target: "go/github.com/other/[email protected]",
|
||||
Active: true,
|
||||
},
|
||||
},
|
||||
scan: &metadata.ScanResult{
|
||||
ID: "r", Scanner: "stub", ScannedAt: time.Now(), Status: metadata.ScanStatusVulnerable,
|
||||
Vulnerabilities: []metadata.Vulnerability{
|
||||
{ID: "CVE-1", Severity: "HIGH"},
|
||||
},
|
||||
},
|
||||
}
|
||||
mgr := newTestManager(t, store)
|
||||
|
||||
blocked, _, err := mgr.CheckVulnerabilities(
|
||||
context.Background(), "go", "github.com/foo/bar/@v/v1.2.3.zip", "v1.2.3")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, blocked, "a non-matching bypass should not unblock the package")
|
||||
}
|
||||
|
||||
@@ -91,17 +91,30 @@ func (fs *FilesystemStorage) Put(ctx context.Context, key string, data io.Reader
|
||||
metrics.RecordStorageOperation("filesystem", "put", "error")
|
||||
return err
|
||||
}
|
||||
dir := filepath.Dir(path)
|
||||
relPath, err := fs.relativeToBase(path)
|
||||
if err != nil {
|
||||
metrics.RecordStorageOperation("filesystem", "put", "error")
|
||||
return err
|
||||
}
|
||||
dir := filepath.Dir(relPath)
|
||||
root, err := os.OpenRoot(fs.basePath)
|
||||
if err != nil {
|
||||
metrics.RecordStorageOperation("filesystem", "put", "error")
|
||||
return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to open storage root")
|
||||
}
|
||||
defer root.Close() // #nosec G307 -- Best-effort cleanup of opened storage root
|
||||
|
||||
// Create directory
|
||||
if mkErr := os.MkdirAll(dir, 0750); mkErr != nil {
|
||||
metrics.RecordStorageOperation("filesystem", "put", "error")
|
||||
return errors.Wrap(mkErr, errors.ErrCodeStorageFailure, "failed to create directory")
|
||||
if dir != "." {
|
||||
if mkErr := root.MkdirAll(dir, 0750); mkErr != nil {
|
||||
metrics.RecordStorageOperation("filesystem", "put", "error")
|
||||
return errors.Wrap(mkErr, errors.ErrCodeStorageFailure, "failed to create directory")
|
||||
}
|
||||
}
|
||||
|
||||
// Create temp file for atomic write
|
||||
tempPath := path + ".tmp"
|
||||
tempFile, err := os.Create(tempPath) // #nosec G304 -- Temp path is constructed from sanitized storage key
|
||||
tempPath := relPath + ".tmp"
|
||||
tempFile, err := root.Create(tempPath)
|
||||
if err != nil {
|
||||
metrics.RecordStorageOperation("filesystem", "put", "error")
|
||||
return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to create temp file")
|
||||
@@ -115,14 +128,14 @@ func (fs *FilesystemStorage) Put(ctx context.Context, key string, data io.Reader
|
||||
|
||||
written, err := io.Copy(multiWriter, data)
|
||||
if err != nil {
|
||||
_ = tempFile.Close() // #nosec G104 -- Cleanup, error not critical
|
||||
_ = os.Remove(tempPath) // #nosec G104 -- Cleanup, error not critical
|
||||
_ = tempFile.Close() // #nosec G104 -- Cleanup, error not critical
|
||||
_ = root.Remove(tempPath)
|
||||
metrics.RecordStorageOperation("filesystem", "put", "error")
|
||||
return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to write data")
|
||||
}
|
||||
|
||||
if err := tempFile.Close(); err != nil {
|
||||
_ = os.Remove(tempPath) // #nosec G104 -- Cleanup, error not critical
|
||||
_ = root.Remove(tempPath)
|
||||
metrics.RecordStorageOperation("filesystem", "put", "error")
|
||||
return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to close temp file")
|
||||
}
|
||||
@@ -133,13 +146,13 @@ func (fs *FilesystemStorage) Put(ctx context.Context, key string, data io.Reader
|
||||
sha256Sum := hex.EncodeToString(sha256Hash.Sum(nil))
|
||||
|
||||
if opts.ChecksumMD5 != "" && opts.ChecksumMD5 != md5Sum {
|
||||
_ = os.Remove(tempPath) // #nosec G104 -- Cleanup, error not critical
|
||||
_ = root.Remove(tempPath)
|
||||
metrics.RecordStorageOperation("filesystem", "put", "checksum_error")
|
||||
return errors.New(errors.ErrCodeChecksumMismatch, "MD5 checksum mismatch")
|
||||
}
|
||||
|
||||
if opts.ChecksumSHA256 != "" && opts.ChecksumSHA256 != sha256Sum {
|
||||
_ = os.Remove(tempPath) // #nosec G104 -- Cleanup, error not critical
|
||||
_ = root.Remove(tempPath)
|
||||
metrics.RecordStorageOperation("filesystem", "put", "checksum_error")
|
||||
return errors.New(errors.ErrCodeChecksumMismatch, "SHA256 checksum mismatch")
|
||||
}
|
||||
@@ -151,13 +164,13 @@ func (fs *FilesystemStorage) Put(ctx context.Context, key string, data io.Reader
|
||||
fs.mu.Lock()
|
||||
if fs.quota > 0 && fs.used+written > fs.quota {
|
||||
fs.mu.Unlock()
|
||||
_ = os.Remove(tempPath) // #nosec G104 -- Cleanup, error not critical
|
||||
_ = root.Remove(tempPath)
|
||||
metrics.RecordStorageOperation("filesystem", "put", "quota_exceeded")
|
||||
return errors.QuotaExceeded(fs.quota)
|
||||
}
|
||||
if err := os.Rename(tempPath, path); err != nil {
|
||||
if err := root.Rename(tempPath, relPath); err != nil {
|
||||
fs.mu.Unlock()
|
||||
_ = os.Remove(tempPath) // #nosec G104 -- Cleanup, error not critical
|
||||
_ = root.Remove(tempPath)
|
||||
metrics.RecordStorageOperation("filesystem", "put", "error")
|
||||
return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to rename temp file")
|
||||
}
|
||||
@@ -408,22 +421,42 @@ func (fs *FilesystemStorage) keyToPath(key string) (string, error) {
|
||||
}
|
||||
|
||||
target := filepath.Join(fs.basePath, cleaned)
|
||||
|
||||
// Defense-in-depth: verify the resolved absolute path is contained
|
||||
// within the base directory.
|
||||
targetAbs, err := filepath.Abs(target)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to resolve path")
|
||||
}
|
||||
if err := fs.ensureWithinBase(targetAbs); err != nil {
|
||||
return "", errors.New(errors.ErrCodeStorageFailure, fmt.Sprintf("path traversal rejected: %s", key))
|
||||
}
|
||||
|
||||
return targetAbs, nil
|
||||
}
|
||||
|
||||
func (fs *FilesystemStorage) ensureWithinBase(path string) error {
|
||||
rel, err := fs.relativeToBase(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
|
||||
return errors.New(errors.ErrCodeStorageFailure, fmt.Sprintf("path traversal rejected: %s", path))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *FilesystemStorage) relativeToBase(path string) (string, error) {
|
||||
baseAbs, err := filepath.Abs(fs.basePath)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to resolve base path")
|
||||
}
|
||||
if targetAbs != baseAbs && !strings.HasPrefix(targetAbs, baseAbs+string(os.PathSeparator)) {
|
||||
return "", errors.New(errors.ErrCodeStorageFailure, fmt.Sprintf("path traversal rejected: %s", key))
|
||||
pathAbs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to resolve path")
|
||||
}
|
||||
|
||||
return target, nil
|
||||
rel, err := filepath.Rel(baseAbs, pathAbs)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to compare path")
|
||||
}
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// calculateUsage calculates current storage usage
|
||||
|
||||
+28
-1
@@ -130,7 +130,18 @@ func (s *Storage) Put(ctx context.Context, key string, data io.Reader, opts *sto
|
||||
s.logger.Warn().Err(err).Str("key", key).Msg("nfs: post-put path lookup failed; skipping fsync")
|
||||
return nil
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_RDWR, 0) // #nosec G304 -- path resolved by sanitizing backend
|
||||
relPath, err := relativeToBase(s.path, path)
|
||||
if err != nil {
|
||||
s.logger.Warn().Err(err).Str("key", key).Msg("nfs: post-put path escaped configured base; skipping fsync")
|
||||
return nil
|
||||
}
|
||||
root, err := os.OpenRoot(s.path)
|
||||
if err != nil {
|
||||
s.logger.Warn().Err(err).Str("path", s.path).Msg("nfs: failed to open configured base; skipping fsync")
|
||||
return nil
|
||||
}
|
||||
defer root.Close() // #nosec G307 -- Best-effort cleanup of opened NFS root
|
||||
f, err := root.OpenFile(relPath, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
s.logger.Warn().Err(err).Str("key", key).Msg("nfs: post-put open failed; skipping fsync")
|
||||
return nil
|
||||
@@ -226,6 +237,22 @@ func (s *Storage) GetLocalPath(ctx context.Context, key string) (string, error)
|
||||
return s.fs.GetLocalPath(ctx, key)
|
||||
}
|
||||
|
||||
func relativeToBase(basePath, path string) (string, error) {
|
||||
baseAbs, err := filepath.Abs(basePath)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, errors.ErrCodeStorageFailure, "nfs: failed to resolve base path")
|
||||
}
|
||||
pathAbs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, errors.ErrCodeStorageFailure, "nfs: failed to resolve path")
|
||||
}
|
||||
rel, err := filepath.Rel(baseAbs, pathAbs)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, errors.ErrCodeStorageFailure, "nfs: failed to compare path")
|
||||
}
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// detectMountType returns the filesystem type backing path. Linux-only: on
|
||||
// other platforms the second return value is false. Implementation walks
|
||||
// /proc/mounts and selects the longest matching mount point, which is the
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hirochachacha/go-smb2"
|
||||
"github.com/cloudsoda/go-smb2"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/errors"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/storage"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -114,7 +114,7 @@ func (s *SMBStorage) createConnection() (*smbConnection, error) {
|
||||
}
|
||||
|
||||
// Establish SMB session
|
||||
session, err := d.Dial(conn)
|
||||
session, err := d.DialConn(context.Background(), conn, addr)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("failed to establish SMB session: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user