Compare commits

...
77 Commits
Author SHA1 Message Date
github-actions[bot]andGitHub 80928c6e21 chore(deps): update dependency @vueuse/core to v15
Automated dependency update. All tests passed.
2026-10-03 17:19:50 +00:00
github-actions[bot]andGitHub 5bcbe08573 chore(deps): update dependency @testing-library/jest-dom to v7
Automated dependency update. All tests passed.
2026-10-03 17:18:18 +00:00
github-actions[bot]andGitHub 5c86b9a5e6 chore(deps): update dependency pinia to v4
Automated dependency update. All tests passed.
2026-10-03 17:16:50 +00:00
github-actions[bot]andGitHub e246206e9c chore(deps): update dependency lucide-vue-next to v1
Automated dependency update. All tests passed.
2026-10-03 17:15:47 +00:00
github-actions[bot]andGitHub 3c33bcdd61 chore(deps): update module github.com/cenkalti/backoff/v4 to v7
Automated dependency update. All tests passed.
2026-10-03 17:13:15 +00:00
github-actions[bot]andGitHub cc7d822a0a chore(deps): update dependency postcss-selector-parser@<6.1.4 to v7
Automated dependency update. All tests passed.
2026-10-03 16:50:54 +00:00
github-actions[bot]andGitHub 30e113fbbe chore(deps): update dependency minimatch@<9.0.9 to v10
Automated dependency update. All tests passed.
2026-10-03 16:48:32 +00:00
github-actions[bot]andGitHub cbceb6101b chore(deps): update dependency marked to v18
Automated dependency update. All tests passed.
2026-10-03 16:47:22 +00:00
github-actions[bot]andGitHub 3b6d2e7258 chore(deps): update dependency jsdom to v30
Automated dependency update. All tests passed.
2026-10-03 16:45:11 +00:00
github-actions[bot]andGitHub 328f764024 chore(deps): update module github.com/gofiber/fiber/v2 to v3
Automated dependency update. All tests passed.
2026-10-03 16:44:01 +00:00
github-actions[bot]andGitHub 8f5ed9c972 chore(deps): update module github.com/cenkalti/backoff/v4 to v7
Automated dependency update. All tests passed.
2026-10-03 16:39:46 +00:00
github-actions[bot]andGitHub 21d004eae9 chore(deps): update dependencies
Automated dependency update. All tests passed.
2026-10-03 16:35:25 +00:00
github-actions[bot]andGitHub 436633bc68 chore(deps): update dependencies
Automated dependency update. All tests passed.
2026-10-03 16:12:39 +00:00
lukaszraczyloandGitHub 8fae680cbe Update go.mod and go.sum (#151) 2026-10-03 04:29:22 +01:00
lukaszraczyloGitHubcopilot-swe-agent[bot] <[email protected]>
b172788741 fix(frontend): override vulnerable transitive dependencies (#150)
Dependabot security updates report update_not_possible because the
vulnerable packages are transitive and their parents pin them. The
Dependabot job fails on every run.

pnpm overrides raise minimatch to 9.0.9, esbuild to 0.28.2 and
postcss-selector-parser to 6.1.4. All three versions are older than the
seven day minimum release age. pnpm audit reports no known
vulnerabilities. The build and the 47 tests pass.

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-10-03 00:56:29 +01:00
lukaszraczylo 48c12a4869 fix(deps): override vulnerable frontend packages
Dependabot could not raise esbuild, minimatch and postcss-selector-parser, because their parents pin older ranges. The workspace now overrides them to the patched versions. The frontend builds and its tests pass.
2026-10-03 00:30:11 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8396284de3 chore(deps): bump the npm_and_yarn group across 1 directory with 8 updates (#148)
Bumps the npm_and_yarn group with 8 updates in the /frontend directory:

| Package | From | To |
| --- | --- | --- |
| [axios](https://github.com/axios/axios) | `1.18.0` | `1.20.0` |
| [@vitest/mocker](https://github.com/vitest-dev/vitest/tree/HEAD/packages/mocker) | `4.1.0` | `5.0.3` |
| [brace-expansion](https://github.com/juliangruber/brace-expansion) | `2.0.2` | `2.1.7` |
| [browserslist](https://github.com/browserslist/browserslist) | `4.28.1` | `4.29.3` |
| [defu](https://github.com/unjs/defu) | `6.1.4` | `6.1.7` |
| [js-cookie](https://github.com/js-cookie/js-cookie) | `3.0.5` | `3.0.8` |
| [picomatch](https://github.com/micromatch/picomatch) | `2.3.1` | `2.3.2` |
| [ws](https://github.com/websockets/ws) | `8.18.3` | `8.22.0` |



Updates `axios` from 1.18.0 to 1.20.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.18.0...v1.20.0)

Updates `@vitest/mocker` from 4.1.0 to 5.0.3
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.3/packages/mocker)

Updates `brace-expansion` from 2.0.2 to 2.1.7
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v2.0.2...v2.1.7)

Updates `browserslist` from 4.28.1 to 4.29.3
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.28.1...4.29.3)

Updates `defu` from 6.1.4 to 6.1.7
- [Release notes](https://github.com/unjs/defu/releases)
- [Changelog](https://github.com/unjs/defu/blob/main/CHANGELOG.md)
- [Commits](https://github.com/unjs/defu/compare/v6.1.4...v6.1.7)

Updates `js-cookie` from 3.0.5 to 3.0.8
- [Release notes](https://github.com/js-cookie/js-cookie/releases)
- [Commits](https://github.com/js-cookie/js-cookie/compare/v3.0.5...v3.0.8)

Updates `picomatch` from 2.3.1 to 2.3.2
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

Updates `ws` from 8.18.3 to 8.22.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.3...8.22.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.20.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: "@vitest/mocker"
  dependency-version: 5.0.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: brace-expansion
  dependency-version: 2.1.7
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: browserslist
  dependency-version: 4.29.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: defu
  dependency-version: 6.1.7
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: js-cookie
  dependency-version: 3.0.8
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: ws
  dependency-version: 8.22.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 13:56:28 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d57a21817b chore(deps): bump go.opentelemetry.io/otel/sdk (#149)
Bumps the go_modules group with 1 update in the / directory: [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go).


Updates `go.opentelemetry.io/otel/sdk` from 1.44.0 to 1.45.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.44.0...v1.45.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.45.0
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 13:56:19 +01:00
lukaszraczylo 1ebb4cfd95 fix(smb): use the cloudsoda go-smb2 fork
hirochachacha/go-smb2 has no fix for GO-2026-5051, an out-of-bounds read in ReadDir that govulncheck reports as called from walkPath. The cloudsoda fork carries the fix. Dialer.DialConn replaces Dial.
2026-10-02 13:24:13 +01:00
lukaszraczylo 8112b4859f fix(auth): restore ValidationCache.Stop
Stop was deleted while its field and doc comment stayed. The cleanup goroutine could not be stopped, and staticcheck flagged stopOnce as unused.
2026-10-02 13:23:58 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a412b61d8d chore(deps): bump the npm_and_yarn group across 1 directory with 8 updates (#128)
Bumps the npm_and_yarn group with 4 updates in the /frontend directory: [axios](https://github.com/axios/axios), [postcss](https://github.com/postcss/postcss), [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `axios` from 1.13.5 to 1.18.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.5...v1.18.0)

Updates `postcss` from 8.5.6 to 8.5.23
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.6...8.5.23)

Updates `vite` from 7.3.0 to 7.3.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.5/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.5/packages/vite)

Updates `vitest` from 4.0.16 to 4.1.0
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.0/packages/vitest)

Updates `follow-redirects` from 1.15.11 to 1.16.0
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.11...v1.16.0)

Updates `form-data` from 4.0.5 to 4.0.6
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.5...v4.0.6)

Updates `nanoid` from 3.3.11 to 3.3.18
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/3.3.18/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.11...3.3.18)

Updates `rollup` from 4.54.0 to 4.63.1
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.54.0...v4.63.1)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.18.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: postcss
  dependency-version: 8.5.23
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 7.3.5
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: vitest
  dependency-version: 4.1.0
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: follow-redirects
  dependency-version: 1.16.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: form-data
  dependency-version: 4.0.6
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: nanoid
  dependency-version: 3.3.18
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: rollup
  dependency-version: 4.63.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 13:14:56 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cbfb16aebd chore(deps): bump the go_modules group across 1 directory with 3 updates (#129)
Bumps the go_modules group with 3 updates in the / directory: [github.com/moby/go-archive](https://github.com/moby/go-archive), [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) and [google.golang.org/grpc](https://github.com/grpc/grpc-go).


Updates `github.com/moby/go-archive` from 0.2.0 to 0.3.0
- [Release notes](https://github.com/moby/go-archive/releases)
- [Changelog](https://github.com/moby/go-archive/blob/main/changes_test.go)
- [Commits](https://github.com/moby/go-archive/compare/v0.2.0...v0.3.0)

Updates `go.opentelemetry.io/otel` from 1.39.0 to 1.41.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.39.0...v1.41.0)

Updates `google.golang.org/grpc` from 1.79.3 to 1.83.2
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.79.3...v1.83.2)

---
updated-dependencies:
- dependency-name: github.com/moby/go-archive
  dependency-version: 0.3.0
  dependency-type: indirect
- dependency-name: go.opentelemetry.io/otel
  dependency-version: 1.41.0
  dependency-type: indirect
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 13:14:41 +01:00
lukaszraczylo 854245c4f3 ci: push benchmark data to gh-pages
The action wrote results to a local gh-pages branch and never pushed them. The next step then failed because docs/bench does not exist on main. The action now pushes gh-pages itself, and the manual push step is gone.
2026-10-02 12:43:30 +01:00
lukaszraczylo d96e724755 build(frontend): allow esbuild and vue-demi build scripts
pnpm 11 stops a frozen install when a dependency build script is not approved.
2026-10-02 12:04:18 +01:00
lukaszraczylo d0b3d82508 ci: build with Go 1.27 and grant PR checks access
go.mod needs Go 1.26 but the workflows pinned Go 1.25. The pull request workflow also lacked the permissions that the shared workflow requires, so it never started.
2026-10-02 12:04:17 +01:00
lukaszraczyloandGitHub 6d4ce976c7 Update go.mod and go.sum (#147) 2026-10-01 04:24:41 +01:00
lukaszraczyloandGitHub d760757c46 Update go.mod and go.sum (#146) 2026-09-30 04:23:17 +01:00
lukaszraczyloandGitHub 67d87ed66b Update go.mod and go.sum (#145) 2026-09-29 04:20:53 +01:00
lukaszraczyloandGitHub 22fc1b454b Update go.mod and go.sum (#144) 2026-09-28 04:23:33 +01:00
lukaszraczyloandGitHub 2c483445a1 Update go.mod and go.sum (#143) 2026-09-26 04:19:22 +01:00
lukaszraczyloandGitHub f5beaea3e0 Update go.mod and go.sum (#142) 2026-09-25 04:19:21 +01:00
lukaszraczyloandGitHub 4d61ed1557 Update go.mod and go.sum (#141) 2026-09-24 04:18:16 +01:00
lukaszraczyloandGitHub f1bcfa02f5 Update go.mod and go.sum (#140) 2026-09-22 04:19:59 +01:00
lukaszraczyloandGitHub 16e09c0515 Update go.mod and go.sum (#139) 2026-09-19 04:17:35 +01:00
lukaszraczyloandGitHub 0ff7719d8a Update go.mod and go.sum (#138) 2026-09-16 04:20:31 +01:00
lukaszraczyloandGitHub 1d7ee6c27b Update go.mod and go.sum (#137) 2026-09-15 04:18:48 +01:00
lukaszraczyloandGitHub e00233943b Update go.mod and go.sum (#136) 2026-09-12 04:19:03 +01:00
lukaszraczyloandGitHub 6c2448bb03 Update go.mod and go.sum (#135) 2026-09-11 04:19:38 +01:00
lukaszraczyloandGitHub a511c380ba Update go.mod and go.sum (#134) 2026-09-10 04:20:11 +01:00
lukaszraczyloandGitHub fa66501e5c Update go.mod and go.sum (#133) 2026-09-09 04:21:00 +01:00
lukaszraczyloandGitHub 35ea5b8a5b Update go.mod and go.sum (#132) 2026-09-08 04:18:09 +01:00
lukaszraczyloandGitHub e5d7242dc1 Update go.mod and go.sum (#131) 2026-09-06 04:19:50 +01:00
lukaszraczyloandGitHub 3c994687ff Update go.mod and go.sum (#130) 2026-09-05 04:19:01 +01:00
lukaszraczylo 171386f7a7 fix: remediate severity-ordered review findings across cache, scanners, and proxies
- H2: gormstore soft-delete + storage_key UNIQUE collision; re-save after eviction succeeds
- H1: cache getOrFetch cache-hit branch no longer runs CheckVulnerabilities on metadata entries
- M2: cache store() streams upstream body to temp file + SHA256 instead of double-buffering
- H3/L3: FetchResult/FetchFunc plumbing (+RequiresAuth/AuthProvider); proxy closures adapted;
        goproxy strips git+ prefix from UpstreamURL
- H4: scanner normalizes bypass package names; CheckVulnerabilities builds normalized packageKey
- M4: requireAdmin routes through extractAPIKey -> ValidateAPIKey -> admin role check
- M5: network client keeps only final RecordFailure (one count per attempt cycle)
- M3: normalizeOriginHost + buildCheckOrigin rewrite; origin_test.go added
- M1: app lifecycle cancel func; Run -> Shutdown on listen error (sole cache owner); JoinHostPort
- M6: VulnerabilityThresholds defaults all -1 so BlockOnSeverity governs
- L4: metadata.NormalizeSeverity at 4 severity-count sites (MEDIUM->MODERATE consistency)
- L1: handleDeleteBypass uses structured *errors.Error + ErrCodeNotFound
- L2: pypi extractVersionFromFilename (wheel/2nd segment, non-wheel/last digit segment);
      cacheVersion uses full unique basename; regression tests added
- L5: document per-instance ValidationCache key scoping; rename packageURL param to resource

All: go build ./... && go vet ./... && go test ./... pass; golangci-lint clean.
2026-09-04 14:24:13 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>copilot-swe-agent[bot] <[email protected]>lukaszraczylo
edeb2eca0c chore(deps): bump google.golang.org/grpc from 1.78.0 to 1.79.3 in the go_modules group across 1 directory (#36)
* chore(deps): bump google.golang.org/grpc

Bumps the go_modules group with 1 update in the / directory: [google.golang.org/grpc](https://github.com/grpc/grpc-go).


Updates `google.golang.org/grpc` from 1.78.0 to 1.79.3
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.78.0...v1.79.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.79.3
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <[email protected]>

* Resolve merge conflicts and harden storage paths

Co-authored-by: lukaszraczylo <[email protected]>

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: lukaszraczylo <[email protected]>
2026-09-03 11:16:29 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7fb596aafd chore(deps): bump axios (#14)
Bumps the npm_and_yarn group with 1 update in the /frontend directory: [axios](https://github.com/axios/axios).


Updates `axios` from 1.13.2 to 1.13.5
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.2...v1.13.5)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.13.5
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-03 10:47:50 +01:00
lukaszraczyloandGitHub eb4517a45d Update go.mod and go.sum (#127) 2026-09-03 05:32:05 +01:00
lukaszraczyloandGitHub fa1b8cf6c4 Update go.mod and go.sum (#126) 2026-09-02 04:20:02 +01:00
lukaszraczyloandGitHub a5ac6f41e2 Update go.mod and go.sum (#125) 2026-09-01 04:20:37 +01:00
lukaszraczyloandGitHub 7de67d6a7a Update go.mod and go.sum (#124) 2026-08-29 04:17:50 +01:00
lukaszraczyloandGitHub db5dfef2d5 Update go.mod and go.sum (#123) 2026-08-28 06:49:21 +01:00
lukaszraczyloandGitHub b6a0a0800f Update go.mod and go.sum (#122) 2026-08-27 05:55:12 +01:00
lukaszraczyloandGitHub 36b99d528e Update go.mod and go.sum (#121) 2026-08-26 04:25:36 +01:00
lukaszraczyloandGitHub 601455cb9b Update go.mod and go.sum (#120) 2026-08-22 04:20:58 +01:00
lukaszraczyloandGitHub e0a489e471 Update go.mod and go.sum (#119) 2026-08-21 04:25:00 +01:00
lukaszraczyloandGitHub aa4b79ab0c Update go.mod and go.sum (#118) 2026-08-18 04:21:39 +01:00
lukaszraczyloandGitHub 8db5aa099b Update go.mod and go.sum (#117) 2026-08-15 04:18:04 +01:00
lukaszraczyloandGitHub 47f63e3b06 Update go.mod and go.sum (#116) 2026-08-13 04:51:47 +01:00
lukaszraczyloandGitHub 43f23900e0 Update go.mod and go.sum (#115) 2026-08-12 04:48:48 +01:00
lukaszraczyloandGitHub 3a113557cd Update go.mod and go.sum (#114) 2026-08-11 04:36:08 +01:00
lukaszraczyloandGitHub 22163595fd Update go.mod and go.sum (#113) 2026-08-08 04:35:19 +01:00
lukaszraczyloandGitHub 4dc7e2fcfb Update go.mod and go.sum (#112) 2026-08-07 05:01:46 +01:00
lukaszraczyloandGitHub 557ebc8410 Update go.mod and go.sum (#111) 2026-08-06 05:05:19 +01:00
lukaszraczyloandGitHub 2c82cd8a7a Update go.mod and go.sum (#110) 2026-08-04 06:51:26 +01:00
lukaszraczyloandGitHub 47cc2900a6 Update go.mod and go.sum (#109) 2026-08-01 06:58:29 +01:00
lukaszraczyloandGitHub c6e7c0d4c8 Update go.mod and go.sum (#108) 2026-07-30 06:45:43 +01:00
lukaszraczyloandGitHub c6ad90f742 Update go.mod and go.sum (#107) 2026-07-29 06:56:36 +01:00
lukaszraczyloandGitHub dfc7d0c917 Update go.mod and go.sum (#106) 2026-07-28 06:51:45 +01:00
lukaszraczyloandGitHub 4442cb3062 Update go.mod and go.sum (#105) 2026-07-27 07:33:24 +01:00
lukaszraczyloandGitHub 3ce6d274d5 Update go.mod and go.sum (#104) 2026-07-25 06:48:25 +01:00
lukaszraczyloandGitHub ce7cb769b3 Update go.mod and go.sum (#103) 2026-07-24 06:54:19 +01:00
lukaszraczyloandGitHub f3a210fd8d Update go.mod and go.sum (#102) 2026-07-23 06:59:04 +01:00
lukaszraczyloandGitHub c7f05c426f Update go.mod and go.sum (#101) 2026-07-22 06:55:12 +01:00
lukaszraczyloandGitHub bd3e8f024c Update go.mod and go.sum (#100) 2026-07-21 06:54:11 +01:00
lukaszraczyloandGitHub c390074f1c Update go.mod and go.sum (#99) 2026-07-17 06:46:38 +01:00
lukaszraczyloandGitHub 43d39bb07a Update go.mod and go.sum (#98) 2026-07-14 06:35:56 +01:00
lukaszraczyloandGitHub a95cd85155 Update go.mod and go.sum (#97) 2026-07-13 07:23:08 +01:00
31 changed files with 2285 additions and 1713 deletions
+1 -1
View File
@@ -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
+6 -1
View File
@@ -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"
+3 -13
View File
@@ -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
View File
@@ -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"
}
}
+1071 -1164
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -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
+85 -79
View File
@@ -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
)
+219 -182
View File
@@ -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
View File
@@ -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
View File
@@ -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
+9 -17
View File
@@ -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")
+13 -5
View File
@@ -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{}{
+73
View File
@@ -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))
}
+33 -20
View File
@@ -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() {
+33
View File
@@ -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
+98 -53
View File
@@ -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")
}
+19 -19
View File
@@ -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)
+10
View File
@@ -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,
+15 -1
View File
@@ -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
-7
View File
@@ -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
}
+35
View File
@@ -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")
}
+54 -30
View File
@@ -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
View File
@@ -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
View File
@@ -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
+70
View File
@@ -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
View File
@@ -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
+76
View File
@@ -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")
}
+54 -21
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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)