diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5aebe1b..bebb9ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,23 @@ jobs: go-version-file: go.mod - run: make lint + docs: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version-file: go.mod + # cmd/docgen regenerates the site's content from rules/*.go and README.md, and rewrites + # README's rules table from the same rule catalog; a diff here means one of those drifted + # from the other and a commit forgot to run "make site-content". + - run: make site-content + - run: git diff --exit-code README.md + dogfooding: runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..dff4e38 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,69 @@ +name: Pages + +# Builds the documentation site in docs/ and publishes it to GitHub Pages. A pull request builds it +# too, without deploying, so a page that breaks the build is caught before it is merged. +on: + push: + branches: [main] + paths: + - docs/** + - rules/** + - linter/rule.go + - cmd/docgen/** + - README.md + - Makefile + - .github/workflows/pages.yml + pull_request: + paths: + - docs/** + - rules/** + - linter/rule.go + - cmd/docgen/** + - README.md + - Makefile + - .github/workflows/pages.yml + workflow_dispatch: + +permissions: {} + +# One deployment at a time, and never cancel one in flight: a half-published site is worse than a +# late one. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + # Hugo is a tool dependency in go.mod, so the Go toolchain is all this needs: "make site" + # builds it at the pinned version and caches it like any other dependency. + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version-file: go.mod + + - run: make site + + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: docs/public + + deploy: + needs: build + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.gitignore b/.gitignore index 7d24a76..4a85103 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,15 @@ profile.cov # Build output /bin/ +# A stray "go build ./cmd/docgen/..." from the repo root lands here; the Makefile always uses +# "go run" for it instead, so this is never an intentional artifact. +/docgen +/docs/public/ +/docs/resources/ +# Rule pages and the README-derived pages; see cmd/docgen and "make site-content". +/docs/.generated/ +# Hugo writes its build lock in the directory it runs from, which is not always docs/. +.hugo_build.lock # Go workspace file go.work diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 11c1c31..6011277 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,16 @@ make lint # golangci-lint make run ARGS="-format=json path/to/dir" ``` +The documentation site in [`docs/`](docs/) is built with +[Hugo](https://gohugo.io/). It is a tool dependency in +[`go.mod`](go.mod), so there is nothing to install and the version is +pinned with the rest of them: + +```console +make site # build into docs/public +make site-serve # serve with live reload at http://localhost:1313/ +``` + ## Adding a rule Rules are plain Go code. Declare a @@ -40,6 +50,10 @@ from its category (see `categoryDefaultSeverities` in default, at `error`. Pick the category that matches the problem the rule reports, not the severity you'd like it to have. +Besides the short `Description`, a rule carries the reasoning and an +example directly on the [`linter.Rule`](linter/rule.go) value — +nothing about a rule lives in a separate file: + ```go package rules @@ -48,11 +62,28 @@ import "github.com/bare-devcontainer/decolint/linter" var MyRule = &linter.Rule{ ID: "my-rule", Description: "...", - Category: linter.CategoryCorrectness, - FileTypes: []linter.FileType{linter.Devcontainer}, - Platforms: nil, // applies to every platform - Paths: []string{"/mounts/*"}, - Check: checkMyRule, + LongDescription: `What goes wrong in the configuration this reports, and what to do +instead.`, + References: []string{"https://containers.dev/implementors/json_reference/"}, + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Devcontainer}, + Platforms: nil, // applies to every platform + Paths: []string{"/mounts/*"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: "devcontainer.json", Content: `{ ... } +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: "devcontainer.json", Content: `{ ... } +`}, + }, + }, + }, + Check: checkMyRule, } func checkMyRule(ctx *linter.Context, node *linter.Node) []linter.Finding { @@ -63,10 +94,50 @@ func checkMyRule(ctx *linter.Context, node *linter.Node) []linter.Finding { } ``` +`LongDescription` is Markdown; write it for the user who just hit the +finding, since `decolint -rules -format=json` and the SARIF output +both carry it, and it is what the documentation site is built from +(see below). + +`Example` is machine-checked, not just illustrative: +[`rules/doc_test.go`](rules/doc_test.go) lints `Bad` with the rule as +the only one active and requires a finding, then lints `Good` and +requires none. `Snippet.Files` is one directory: the file named after +the rule's first `FileTypes` entry (`devcontainer.json`, +`devcontainer-feature.json`, or `devcontainer-template.json`) is the +one linted, and any other files are context a rule reads from the +directory (e.g. a Template's other files, for a +`${templateOption:...}` reference). Set `Snippet.DirName` when the +rule reads the directory's own name (`id-dir-mismatch`), and a file's +`Mode` when the rule reads permission bits (`install.sh`'s executable +bit) — `Bad` and `Good` can then differ in mode alone, with identical +content. `Example.Note` is optional Markdown shown after `Good`, for +context the two snippets alone don't convey. + The existing rules in [`rules/`](rules/) are good references, -including for the table-driven tests each rule ships with. When a new -rule lands, also add a row for it to the table in -[README.md](README.md#rules). +including for the table-driven tests each rule ships with. + +## The documentation site and the README rules table + +Both are generated from `rules/*.go` and `README.md` by +[`cmd/docgen`](cmd/docgen/), run as part of `make site` (see +[Development](#development) above) and standalone as `make +site-content`. Nothing under `docs/content/rules/` other than +`_index.md`, and nothing in `README.md` between the +`` markers, is hand-edited — a new rule +or a changed `LongDescription`/`Example`/`References` needs no +follow-up edit anywhere else. CI's `docs` job runs `make site-content` +and fails if that changes `README.md`, which is what catches a +generator or a rule declaration that drifted from the other. + +`README.md` itself is also this generator's input for the rest of the +site: the landing page, Getting started, and Reference are the +corresponding sections of `README.md`, split at their headings. A +link within README.md to a heading that ends up on a different page +(e.g. Getting started linking to `#config-file`, which lives on +Reference) is rewritten to point there; write new cross-references the +same way you already do (`[Config file](#config-file)`) and the +generator will resolve them. When implementing or reviewing rules, consult the Dev Container specification at [containers.dev](https://containers.dev/) to confirm diff --git a/Makefile b/Makefile index bb617de..c9b326e 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,13 @@ export GOEXPERIMENT := jsonv2 # renovate: datasource=github-releases depName=golangci/golangci-lint GOLANGCI_LINT_VERSION := v2.12.2 +# Hugo is a tool dependency (see the tool directive in go.mod), so the version the site is built +# with is pinned there and needs nothing installed. HUGO overrides it with another binary. +HUGO ?= go tool hugo + +# Port for site-serve. +SITE_PORT ?= 1313 + VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) REVISION := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) GOBUILDFLAGS := -trimpath -ldflags="-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" @@ -37,9 +44,40 @@ coverage: ## Run tests and open an HTML coverage report lint: ## Run all lint rules go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) run +.PHONY: site +site: site-content ## Build the documentation site into docs/public + $(HUGO) --source docs --minify + +.PHONY: site-serve +site-serve: site-content ## Serve the documentation site with live reload + # hugo server keeps the path of the configured baseURL, which would serve the site under + # /decolint/ and leave http://localhost:$(SITE_PORT)/ a 404. Override it for local preview. + $(HUGO) server --source docs --port $(SITE_PORT) --baseURL http://localhost:$(SITE_PORT)/ + +.PHONY: site-content +site-content: ## Regenerate the rule pages, README-derived pages, and README rules table + go run ./cmd/docgen + +.PHONY: site-syntax +site-syntax: ## Regenerate the syntax highlighting stylesheet + @{ \ + light=$$($(HUGO) gen chromastyles --style=github | tail -n +2); \ + echo '/* Chroma syntax highlighting token colors.'; \ + echo ' Generated by `make site-syntax`; edit that target rather than this file. */'; \ + echo; \ + echo "$$light"; \ + echo '@media (prefers-color-scheme: dark) {'; \ + echo ' /* github-dark omits token classes it colors the same as the default text color,'; \ + echo ' so fall back to inheriting that color instead of the light style'"'"'s, which'; \ + echo ' stays in effect below for every class the dark style does define. */'; \ + echo "$$light" | grep -oE '\.chroma \.[A-Za-z0-9]+' | sort -u | sed 's/.*/ & { color: inherit }/'; \ + $(HUGO) gen chromastyles --style=github-dark | tail -n +2 | sed 's/^./ &/'; \ + echo '}'; \ + } > docs/assets/css/syntax.css + .PHONY: clean clean: ## Remove build artifacts - rm -rf bin coverage.out coverage.html + rm -rf bin coverage.out coverage.html docs/public docs/resources docs/.generated docs/.hugo_build.lock .hugo_build.lock .PHONY: install install: ## Install the decolint binary to GOPATH/bin diff --git a/README.md b/README.md index ba98436..b9c96e3 100644 --- a/README.md +++ b/README.md @@ -581,6 +581,16 @@ optionally target specific platforms (see [Target platforms](#target-platforms)); a rule with no target platform applies to all platforms. +Every rule has a page on the [documentation +site](https://bare-devcontainer.github.io/decolint/rules/) covering +why it exists, the configuration it accepts and rejects, and the +specification it is based on. Each rule ID in the table below links to +its page. + +The [SARIF output](#output-formats) links every rule it reports to the +same page, so the reasoning is one click away from the alert in GitHub +Code Scanning. + ### Rule categories Only `correctness` runs by default; the rest are `off` until enabled: @@ -593,36 +603,37 @@ Only `correctness` runs by default; the rest are `off` until enabled: - `style` (default `off`) — discouraged or legacy configuration that still works. - + | ID | Category | Platform | Description | | --- | --- | --- | --- | -| `conflicting-container-def` | `correctness` | (all) | disallow a devcontainer.json that defines more than one of `image`, `build`, or `dockerComposeFile` | -| `feature-install-script-not-executable` | `correctness` | (all) | disallow a Feature's `install.sh` that lacks executable permission bits | -| `id-dir-mismatch` | `correctness` | (all) | disallow a Feature's or Template's `id` that does not match the name of its containing directory | -| `invalid-semver` | `correctness` | (all) | disallow a Feature's or Template's `version` that is not a valid semantic version | -| `missing-build-dockerfile` | `correctness` | (all) | disallow a devcontainer.json `build` object that is missing `dockerfile` | -| `missing-compose-service` | `correctness` | (all) | disallow a devcontainer.json that sets `dockerComposeFile` without `service` | -| `missing-container-def` | `correctness` | (all) | disallow a devcontainer.json that defines none of `image`, `build`, or `dockerComposeFile` | -| `missing-feature-install-script` | `correctness` | (all) | disallow a Feature directory without the required `install.sh` install script | -| `missing-required-props` | `correctness` | (all) | disallow a Feature's or Template's metadata that is missing a required property (`id`, `version`, or `name`) | -| `missing-workspace-mount-folder` | `correctness` | (all) | disallow a devcontainer.json using `image` or `build` that sets only one of `workspaceMount` or `workspaceFolder` | -| `no-bind-mount` | `correctness` | `codespaces` | disallow `bind` type entries in `mounts`, which GitHub Codespaces silently ignores except for the Docker socket | -| `no-host-port-format` | `correctness` | `codespaces` | disallow `host:port` entries in `forwardPorts` and `portsAttributes`, which GitHub Codespaces does not support | -| `undefined-template-option` | `correctness` | (all) | disallow a `${templateOption:...}` reference to an option not declared in devcontainer-template.json | -| `no-cap-add-all` | `security` | (all) | disallow granting all Linux capabilities via an `ALL` entry in a devcontainer.json's or Feature's `capAdd` property, or a `--cap-add=ALL` entry in a devcontainer.json's `runArgs` | -| `no-docker-socket-mount` | `security` | (all) | disallow bind-mounting the host's Docker socket via a devcontainer.json's `mounts` or `runArgs`, which grants the container root-equivalent control over the host | -| `no-privileged-container` | `security` | (all) | disallow running the container in privileged mode via a devcontainer.json's or Feature's `privileged` property, or a `--privileged` entry in a devcontainer.json's `runArgs` | -| `no-seccomp-override` | `security` | (all) | disallow overriding the container runtime's default seccomp profile via a devcontainer.json's or Feature's `securityOpt` property, or a `--security-opt seccomp=...` entry in a devcontainer.json's `runArgs` | -| `no-seccomp-unconfined` | `security` | (all) | disallow disabling seccomp confinement via a devcontainer.json's or Feature's `securityOpt` property, or a `--security-opt seccomp=unconfined` entry in a devcontainer.json's `runArgs` | -| `require-cap-drop-all` | `security` | (all) | require an `ALL` entry in a devcontainer.json's `capDrop` property, or a `--cap-drop=ALL` entry in `runArgs`, dropping every Linux capability | -| `require-no-new-privileges` | `security` | (all) | require `no-new-privileges` to be set via a devcontainer.json's `securityOpt` property, or a `--security-opt no-new-privileges...` entry in `runArgs` | -| `require-non-root` | `security` | (all) | require `remoteUser` or, if unset, `containerUser` to be set to a non-root user | -| `no-image-latest` | `reproducibility` | (all) | disallow container images without an explicit tag or with the `latest` tag | -| `pin-extension-version` | `reproducibility` | `vscode`, `codespaces` | disallow a `customizations.vscode.extensions` entry without an explicit pinned version | -| `pin-feature-version` | `reproducibility` | (all) | disallow a Feature reference without an explicit version or with the `latest` version | -| `pin-image-digest` | `reproducibility` | (all) | disallow an `image` property that does not pin the image by content digest (e.g. `image@sha256:...`) | -| `no-app-port` | `style` | (all) | disallow the legacy `appPort` property in favor of `forwardPorts` | -| `unused-template-option` | `style` | (all) | disallow a Template option that no file in the Template references | +| [`conflicting-container-def`](https://bare-devcontainer.github.io/decolint/rules/conflicting-container-def/) | `correctness` | (all) | disallow a devcontainer.json that defines more than one of "image", "build", or "dockerComposeFile" | +| [`feature-install-script-not-executable`](https://bare-devcontainer.github.io/decolint/rules/feature-install-script-not-executable/) | `correctness` | (all) | disallow a Feature's `install.sh` that lacks executable permission bits | +| [`id-dir-mismatch`](https://bare-devcontainer.github.io/decolint/rules/id-dir-mismatch/) | `correctness` | (all) | disallow a Feature's or Template's "id" that does not match the name of its containing directory | +| [`invalid-semver`](https://bare-devcontainer.github.io/decolint/rules/invalid-semver/) | `correctness` | (all) | disallow a Feature's or Template's "version" that is not a valid semantic version | +| [`missing-build-dockerfile`](https://bare-devcontainer.github.io/decolint/rules/missing-build-dockerfile/) | `correctness` | (all) | disallow a devcontainer.json "build" object that is missing "dockerfile" | +| [`missing-compose-service`](https://bare-devcontainer.github.io/decolint/rules/missing-compose-service/) | `correctness` | (all) | disallow a devcontainer.json that sets "dockerComposeFile" without "service" | +| [`missing-container-def`](https://bare-devcontainer.github.io/decolint/rules/missing-container-def/) | `correctness` | (all) | disallow a devcontainer.json that defines none of "image", "build", or "dockerComposeFile" | +| [`missing-feature-install-script`](https://bare-devcontainer.github.io/decolint/rules/missing-feature-install-script/) | `correctness` | (all) | disallow a Feature directory without the required `install.sh` install script | +| [`missing-required-props`](https://bare-devcontainer.github.io/decolint/rules/missing-required-props/) | `correctness` | (all) | disallow a Feature's or Template's metadata that is missing a required property ("id", "version", or "name") | +| [`missing-workspace-mount-folder`](https://bare-devcontainer.github.io/decolint/rules/missing-workspace-mount-folder/) | `correctness` | (all) | disallow a devcontainer.json using "image" or "build" that sets only one of "workspaceMount" or "workspaceFolder" | +| [`no-bind-mount`](https://bare-devcontainer.github.io/decolint/rules/no-bind-mount/) | `correctness` | `codespaces` | disallow "bind" type entries in "mounts", which GitHub Codespaces silently ignores except for the Docker socket | +| [`no-host-port-format`](https://bare-devcontainer.github.io/decolint/rules/no-host-port-format/) | `correctness` | `codespaces` | disallow "host:port" entries in "forwardPorts" and "portsAttributes", which GitHub Codespaces does not support | +| [`undefined-template-option`](https://bare-devcontainer.github.io/decolint/rules/undefined-template-option/) | `correctness` | (all) | disallow a `${templateOption:...}` reference to an option not declared in devcontainer-template.json | +| [`no-cap-add-all`](https://bare-devcontainer.github.io/decolint/rules/no-cap-add-all/) | `security` | (all) | disallow granting all Linux capabilities via an "ALL" entry in the "capAdd" property, or a "--cap-add=ALL" entry in a devcontainer.json's "runArgs" | +| [`no-docker-socket-mount`](https://bare-devcontainer.github.io/decolint/rules/no-docker-socket-mount/) | `security` | (all) | disallow bind-mounting the host's Docker socket via a devcontainer.json's "mounts" or "runArgs", which grants the container root-equivalent control over the host | +| [`no-privileged-container`](https://bare-devcontainer.github.io/decolint/rules/no-privileged-container/) | `security` | (all) | disallow running the container in privileged mode via the "privileged" property or a "--privileged" entry in "runArgs" | +| [`no-seccomp-override`](https://bare-devcontainer.github.io/decolint/rules/no-seccomp-override/) | `security` | (all) | disallow overriding the container runtime's default seccomp profile via a devcontainer.json's or Feature's "securityOpt" property, or a "--security-opt seccomp=..." entry in a devcontainer.json's "runArgs" | +| [`no-seccomp-unconfined`](https://bare-devcontainer.github.io/decolint/rules/no-seccomp-unconfined/) | `security` | (all) | disallow disabling seccomp confinement via a devcontainer.json's or Feature's "securityOpt" property, or a "--security-opt seccomp=unconfined" entry in a devcontainer.json's "runArgs" | +| [`require-cap-drop-all`](https://bare-devcontainer.github.io/decolint/rules/require-cap-drop-all/) | `security` | (all) | require a "--cap-drop=ALL" entry in a devcontainer.json's "runArgs", dropping every Linux capability | +| [`require-no-new-privileges`](https://bare-devcontainer.github.io/decolint/rules/require-no-new-privileges/) | `security` | (all) | require "no-new-privileges" to be set via a devcontainer.json's "securityOpt" property, or a "--security-opt no-new-privileges..." entry in "runArgs" | +| [`require-non-root`](https://bare-devcontainer.github.io/decolint/rules/require-non-root/) | `security` | (all) | require "remoteUser" or, if unset, "containerUser" to be set to a non-root user | +| [`no-image-latest`](https://bare-devcontainer.github.io/decolint/rules/no-image-latest/) | `reproducibility` | (all) | disallow container images without an explicit tag or with the "latest" tag | +| [`pin-extension-version`](https://bare-devcontainer.github.io/decolint/rules/pin-extension-version/) | `reproducibility` | `vscode`, `codespaces` | disallow a "customizations.vscode.extensions" entry without an explicit pinned version | +| [`pin-feature-version`](https://bare-devcontainer.github.io/decolint/rules/pin-feature-version/) | `reproducibility` | (all) | disallow a Feature reference without an explicit version or with the "latest" version | +| [`pin-image-digest`](https://bare-devcontainer.github.io/decolint/rules/pin-image-digest/) | `reproducibility` | (all) | disallow an "image" property that does not pin the image by content digest (e.g. "image@sha256:...") | +| [`no-app-port`](https://bare-devcontainer.github.io/decolint/rules/no-app-port/) | `style` | (all) | disallow the legacy "appPort" property in favor of "forwardPorts" | +| [`unused-template-option`](https://bare-devcontainer.github.io/decolint/rules/unused-template-option/) | `style` | (all) | disallow a Template option that no file in the Template references | + ## Suppressing findings diff --git a/cmd/decolint/format.go b/cmd/decolint/format.go index ef657e6..2c93779 100644 --- a/cmd/decolint/format.go +++ b/cmd/decolint/format.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/bare-devcontainer/decolint/format" + "github.com/bare-devcontainer/decolint/linter" "github.com/bare-devcontainer/decolint/rules" ) @@ -44,7 +45,64 @@ func sarifRules(cfg Config) []format.SARIFRule { ID: reg.Rule.ID, Description: reg.Rule.Description, Category: reg.Rule.Category.String(), + HelpURI: rules.DocsURL(reg.Rule.ID), } } return out } + +// ruleDocs adapts every built-in rule, and the severity cfg currently gives it, into the shape +// "decolint -rules -format=json" and cmd/docgen consume, so neither package depends on the rules +// package. +func ruleDocs(cfg Config) []format.RuleDoc { + overrides := rules.Overrides{Categories: cfg.Categories, Rules: cfg.Rules} + builtin := rules.Builtin() + out := make([]format.RuleDoc, len(builtin)) + for i, reg := range builtin { + out[i] = format.RuleDoc{ + ID: reg.Rule.ID, + Description: reg.Rule.Description, + LongDescription: reg.Rule.LongDescription, + References: reg.Rule.References, + Category: reg.Rule.Category.String(), + Platforms: platformStrings(reg.Rule.Platforms), + FileTypes: fileTypeStrings(reg.Rule.FileTypes), + Example: ruleExample(reg.Rule.Example), + DocsURL: rules.DocsURL(reg.Rule.ID), + Severity: overrides.SeverityFor(reg).String(), + } + } + return out +} + +func platformStrings(platforms []linter.Platform) []string { + out := make([]string, len(platforms)) + for i, p := range platforms { + out[i] = p.String() + } + return out +} + +func fileTypeStrings(fileTypes []linter.FileType) []string { + out := make([]string, len(fileTypes)) + for i, ft := range fileTypes { + out[i] = string(ft) + } + return out +} + +func ruleExample(ex linter.Example) format.RuleExample { + return format.RuleExample{ + Bad: ruleSnippet(ex.Bad), + Good: ruleSnippet(ex.Good), + Note: ex.Note, + } +} + +func ruleSnippet(s linter.Snippet) format.RuleSnippet { + files := make([]format.RuleExampleFile, len(s.Files)) + for i, f := range s.Files { + files[i] = format.RuleExampleFile{Path: f.Path, Content: f.Content, Mode: uint32(f.Mode.Perm())} + } + return format.RuleSnippet{Files: files, DirName: s.DirName} +} diff --git a/cmd/decolint/format_test.go b/cmd/decolint/format_test.go index 19bce9e..94e1f15 100644 --- a/cmd/decolint/format_test.go +++ b/cmd/decolint/format_test.go @@ -1,14 +1,54 @@ package main import ( + "io/fs" "slices" "testing" "github.com/bare-devcontainer/decolint/format" "github.com/bare-devcontainer/decolint/linter" + "github.com/bare-devcontainer/decolint/rules" "github.com/google/go-cmp/cmp" ) +// TestSarifRules_Fields checks that the adapter carries every field a SARIF consumer needs to +// describe a rule, the address of its documentation included, so an alert can be traced back to +// what the rule checks and why. +func TestSarifRules_Fields(t *testing.T) { + t.Parallel() + + const id = "no-image-latest" + var reg rules.Registration + for _, r := range rules.Builtin() { + if r.Rule.ID == id { + reg = r + } + } + if reg.Rule == nil { + t.Fatalf("built-in rule %s not found", id) + } + + // A reproducibility rule is off by default, so its category has to be enabled for the catalog + // to describe it at all. + cfg := Config{Categories: map[string]linter.Severity{"reproducibility": linter.SeverityError}} + var got format.SARIFRule + for _, r := range sarifRules(cfg) { + if r.ID == id { + got = r + } + } + + want := format.SARIFRule{ + ID: reg.Rule.ID, + Description: reg.Rule.Description, + Category: reg.Rule.Category.String(), + HelpURI: rules.DocsURL(reg.Rule.ID), + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("sarifRules() entry mismatch (-want +got):\n%s", diff) + } +} + func TestParseFormat(t *testing.T) { t.Parallel() @@ -111,3 +151,19 @@ func TestSarifRules(t *testing.T) { }) } } + +// TestRuleSnippet_ModeIsPermissionBitsOnly checks that ruleSnippet reports only the POSIX +// permission bits (see format.RuleExampleFile.Mode), not the type bits fs.FileMode also carries +// (e.g. fs.ModeDir), which would otherwise inflate the value ruleExample -rules -format=json emits. +func TestRuleSnippet_ModeIsPermissionBitsOnly(t *testing.T) { + t.Parallel() + + snippet := ruleSnippet(linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: "some-dir", Mode: fs.ModeDir | 0o755}, + }, + }) + if got, want := snippet.Files[0].Mode, uint32(0o755); got != want { + t.Errorf("Mode = %#o, want %#o", got, want) + } +} diff --git a/cmd/decolint/main.go b/cmd/decolint/main.go index de26a87..0f26f39 100644 --- a/cmd/decolint/main.go +++ b/cmd/decolint/main.go @@ -2,7 +2,9 @@ package main import ( + "bytes" "context" + "encoding/json/v2" "errors" "flag" "fmt" @@ -90,7 +92,7 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer, getenv fu cfg = mergeConfig(opts, cfg) if opts.ListRules { - if err := listRules(stdout, cfg); err != nil { + if err := listRules(stdout, cfg, opts.Format); err != nil { _, _ = fmt.Fprintln(stderr, progName+":", err) return exitCodeError } @@ -123,27 +125,39 @@ var severityEmoji = map[linter.Severity]string{ // rulesTableHeader is the header row of the -rules Markdown table. var rulesTableHeader = []string{"Rule ID", "Category", "Platform", "Current"} -// listRules writes a Markdown table of the built-in rules to output: each rule's ID, category, -// target platforms (or "(all)"), and current severity (its category's default, overridden by cfg -// if any), in the order rules.Builtin returns them. A rule's default severity is not listed -// separately since it is uniform within a category; see the README's Rule categories section. -// Columns are padded to a common width so the raw Markdown source itself reads as an aligned table. -func listRules(output io.Writer, cfg Config) error { +// listRules writes the built-in rules to output, in the format the format argument names (the raw +// -format flag value, not cfg.Format: a config file's "format" selects the lint-findings format, and +// has no bearing on -rules, which lists the catalog instead): +// - "" or "text" (the default): a Markdown table of each rule's ID, category, target platforms +// (or "(all)"), and current severity (its category's default, overridden by cfg if any). A +// rule's default severity is not listed separately since it is uniform within a category; see +// the README's Rule categories section. +// - "json": the full catalog (description, rationale, references, example, docs address, +// current severity), for tooling — cmd/docgen builds the documentation site from it. +// +// "github" and "sarif" describe lint findings, not the rule catalog itself, so they are an error +// here. +func listRules(output io.Writer, cfg Config, format string) error { + switch strings.ToLower(format) { + case "", "text": + return listRulesText(output, cfg) + case "json": + return listRulesJSON(output, cfg) + default: + return fmt.Errorf("unknown format %q for -rules (want one of: text, json)", format) + } +} + +// listRulesText writes a Markdown table of the built-in rules to output; see [listRules]. Columns +// are padded to a common width so the raw Markdown source itself reads as an aligned table. +func listRulesText(output io.Writer, cfg Config) error { overrides := rules.Overrides{Categories: cfg.Categories, Rules: cfg.Rules} rows := [][]string{rulesTableHeader} for _, reg := range rules.Builtin() { - platforms := "(all)" - if len(reg.Rule.Platforms) > 0 { - names := make([]string, len(reg.Rule.Platforms)) - for i, p := range reg.Rule.Platforms { - names[i] = p.String() - } - platforms = strings.Join(names, ",") - } rows = append(rows, []string{ reg.Rule.ID, reg.Rule.Category.String(), - platforms, + platformNames(reg.Rule.Platforms, ","), severityEmoji[overrides.SeverityFor(reg)], }) } @@ -167,6 +181,34 @@ func listRules(output io.Writer, cfg Config) error { return nil } +// listRulesJSON writes the full rule catalog (see [format.RuleDoc]) to output as a JSON array, one +// entry per built-in rule in rules.Builtin order. It marshals into an in-memory buffer first so +// that a failure never leaves partial JSON on output. +func listRulesJSON(output io.Writer, cfg Config) error { + var buf bytes.Buffer + if err := json.MarshalWrite(&buf, ruleDocs(cfg)); err != nil { + return fmt.Errorf("marshal rules: %w", err) + } + buf.WriteByte('\n') + if _, err := output.Write(buf.Bytes()); err != nil { + return fmt.Errorf("write rules: %w", err) + } + return nil +} + +// platformNames renders the platforms a rule targets, separated by sep. A rule that targets none +// applies to every platform, which is shown as "(all)". +func platformNames(platforms []linter.Platform, sep string) string { + if len(platforms) == 0 { + return "(all)" + } + names := make([]string, len(platforms)) + for i, p := range platforms { + names[i] = p.String() + } + return strings.Join(names, sep) +} + // columnWidths returns, for each column in rows, the display width of its widest cell, so every // row can be padded to a common set of column widths. func columnWidths(rows [][]string) []int { diff --git a/cmd/decolint/main_test.go b/cmd/decolint/main_test.go index e3f41cf..0c5c8bc 100644 --- a/cmd/decolint/main_test.go +++ b/cmd/decolint/main_test.go @@ -452,6 +452,126 @@ func TestRun_Flags(t *testing.T) { } }) + t.Run("-rules -format=json", func(t *testing.T) { + t.Parallel() + + var stdout, stderr bytes.Buffer + exitCode := run(t.Context(), []string{"-rules", "-format=json"}, &stdout, &stderr, emptyEnv) + if exitCode != 0 { + t.Errorf("exit code = %d, want 0", exitCode) + } + if stderr.String() != "" { + t.Errorf("stderr = %q, want empty", stderr.String()) + } + + var got []format.RuleDoc + if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { + t.Fatalf("unmarshal -rules -format=json output: %v", err) + } + + builtin := rules.Builtin() + if len(got) != len(builtin) { + t.Fatalf("got %d rule(s), want %d", len(got), len(builtin)) + } + + i := slices.IndexFunc(got, func(d format.RuleDoc) bool { return d.ID == "no-privileged-container" }) + if i < 0 { + t.Fatalf("no-privileged-container missing from -rules -format=json output") + } + ri := slices.IndexFunc(builtin, func(reg rules.Registration) bool { return reg.Rule.ID == "no-privileged-container" }) + if ri < 0 { + t.Fatalf("no built-in rule with ID %q", "no-privileged-container") + } + rule := builtin[ri].Rule + want := format.RuleDoc{ + ID: rule.ID, + Description: rule.Description, + LongDescription: rule.LongDescription, + References: rule.References, + Category: rule.Category.String(), + Platforms: []string{}, + FileTypes: []string{"devcontainer", "feature"}, + DocsURL: rules.DocsURL(rule.ID), + Severity: linter.SeverityOff.String(), + Example: format.RuleExample{ + Bad: format.RuleSnippet{ + Files: []format.RuleExampleFile{ + {Path: rule.Example.Bad.Files[0].Path, Content: rule.Example.Bad.Files[0].Content}, + }, + }, + Good: format.RuleSnippet{ + Files: []format.RuleExampleFile{ + {Path: rule.Example.Good.Files[0].Path, Content: rule.Example.Good.Files[0].Content}, + }, + }, + Note: rule.Example.Note, + }, + } + if diff := cmp.Diff(want, got[i]); diff != "" { + t.Errorf("no-privileged-container doc mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("-rules -format=json carries example file mode", func(t *testing.T) { + t.Parallel() + + var stdout, stderr bytes.Buffer + exitCode := run(t.Context(), []string{"-rules", "-format=json"}, &stdout, &stderr, emptyEnv) + if exitCode != 0 { + t.Errorf("exit code = %d, want 0", exitCode) + } + + var got []format.RuleDoc + if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { + t.Fatalf("unmarshal -rules -format=json output: %v", err) + } + i := slices.IndexFunc(got, func(d format.RuleDoc) bool { return d.ID == "feature-install-script-not-executable" }) + if i < 0 { + t.Fatalf("feature-install-script-not-executable missing from -rules -format=json output") + } + j := slices.IndexFunc(got[i].Example.Good.Files, func(f format.RuleExampleFile) bool { return f.Path == "install.sh" }) + if j < 0 { + t.Fatalf("Good example has no install.sh file") + } + if got[i].Example.Good.Files[j].Mode != 0o755 { + t.Errorf("Good install.sh mode = %#o, want 0755", got[i].Example.Good.Files[j].Mode) + } + }) + + t.Run("-rules -format=sarif is an error", func(t *testing.T) { + t.Parallel() + + var stdout, stderr bytes.Buffer + exitCode := run(t.Context(), []string{"-rules", "-format=sarif"}, &stdout, &stderr, emptyEnv) + if exitCode != 2 { + t.Errorf("exit code = %d, want 2", exitCode) + } + if stdout.String() != "" { + t.Errorf("stdout = %q, want empty", stdout.String()) + } + if !strings.Contains(stderr.String(), "sarif") { + t.Errorf("stderr = %q, want it to mention the unsupported format", stderr.String()) + } + }) + + t.Run("-rules ignores the config file's format", func(t *testing.T) { + t.Parallel() + + var stdout, stderr bytes.Buffer + exitCode := run(t.Context(), []string{"-rules", "-config=testdata/e2e/format-sarif.jsonc"}, &stdout, &stderr, emptyEnv) + if exitCode != 0 { + t.Errorf("exit code = %d, want 0", exitCode) + } + if stderr.String() != "" { + t.Errorf("stderr = %q, want empty", stderr.String()) + } + + header := mdTableRow(t, stdout.String(), rulesTableHeader[0]) + if diff := cmp.Diff(rulesTableHeader, header); diff != "" { + t.Errorf("header row mismatch (-want +got):\n%s", diff) + } + }) + t.Run("-help", func(t *testing.T) { t.Parallel() diff --git a/cmd/decolint/testdata/e2e/format-sarif.jsonc b/cmd/decolint/testdata/e2e/format-sarif.jsonc new file mode 100644 index 0000000..6fec8e0 --- /dev/null +++ b/cmd/decolint/testdata/e2e/format-sarif.jsonc @@ -0,0 +1,5 @@ +// Config for the e2e test: a project whose lint-findings format is "sarif". -rules lists the rule +// catalog, not lint findings, so this member must not affect it. +{ + "format": "sarif" +} diff --git a/cmd/docgen/main.go b/cmd/docgen/main.go new file mode 100644 index 0000000..1fae78a --- /dev/null +++ b/cmd/docgen/main.go @@ -0,0 +1,44 @@ +// Command docgen generates the documentation site's content and the README's rules table from +// rules/*.go and README.md, so neither has to be hand-kept in sync with the other. It is not part of +// the decolint binary; "make site" runs it before Hugo builds the site (see the Makefile). +package main + +import ( + "fmt" + "os" +) + +// generatedContentDir is where the generated site pages are written. It is mounted alongside +// docs/content in hugo.toml and is never committed (see .gitignore); docs/content/rules/_index.md +// is the only rules page that stays hand-written. +const generatedContentDir = "docs/.generated/content" + +func main() { + if err := run("README.md", generatedContentDir); err != nil { + fmt.Fprintln(os.Stderr, "docgen:", err) + os.Exit(1) + } +} + +// run regenerates everything docgen owns: the rule pages and the README-derived pages into +// contentDir, and the rules table in the README at readmePath, in place. +func run(readmePath, contentDir string) error { + if err := os.RemoveAll(contentDir); err != nil { + return fmt.Errorf("clean %s: %w", contentDir, err) + } + if err := os.MkdirAll(contentDir, 0o755); err != nil { + return fmt.Errorf("create %s: %w", contentDir, err) + } + if err := writeRulePages(contentDir); err != nil { + return fmt.Errorf("generate rule pages: %w", err) + } + // Update the README's own table before splitting it into pages below, so a stale table (e.g. + // right after a rule is added or removed) doesn't get carried into the generated reference page. + if err := updateReadmeRulesTable(readmePath); err != nil { + return fmt.Errorf("update rules table in %s: %w", readmePath, err) + } + if err := writeReadmePages(readmePath, contentDir); err != nil { + return fmt.Errorf("generate pages from %s: %w", readmePath, err) + } + return nil +} diff --git a/cmd/docgen/main_test.go b/cmd/docgen/main_test.go new file mode 100644 index 0000000..b14fe59 --- /dev/null +++ b/cmd/docgen/main_test.go @@ -0,0 +1,87 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/bare-devcontainer/decolint/rules" +) + +func TestRun(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + readmePath := filepath.Join(dir, "README.md") + if err := os.WriteFile(readmePath, []byte(fixtureReadme), 0o644); err != nil { + t.Fatalf("write fixture README: %v", err) + } + contentDir := filepath.Join(dir, "content") + + if err := run(readmePath, contentDir); err != nil { + t.Fatalf("run: %v", err) + } + + for _, name := range []string{"_index.md", "getting-started.md", "reference.md"} { + if _, err := os.Stat(filepath.Join(contentDir, name)); err != nil { + t.Errorf("run did not create %s: %v", name, err) + } + } + entries, err := os.ReadDir(filepath.Join(contentDir, "rules")) + if err != nil { + t.Fatalf("read rules dir: %v", err) + } + if len(entries) != len(rules.Builtin()) { + t.Errorf("run wrote %d rule page(s), want %d", len(entries), len(rules.Builtin())) + } + + got, err := os.ReadFile(readmePath) + if err != nil { + t.Fatalf("read %s: %v", readmePath, err) + } + if string(got) == fixtureReadme { + t.Error("run did not touch the README's rules table (fixture has a stale placeholder row)") + } + + // reference.md is split from the README, so it must carry the table run just refreshed above, + // not the fixture's stale placeholder row that was on disk when run started. + reference, err := os.ReadFile(filepath.Join(contentDir, "reference.md")) + if err != nil { + t.Fatalf("read reference.md: %v", err) + } + if strings.Contains(string(reference), "`x`") { + t.Error("reference.md still has the fixture's stale placeholder row") + } + if !strings.Contains(string(reference), rules.Builtin()[0].Rule.ID) { + t.Errorf("reference.md missing rule ID %q from the refreshed table", rules.Builtin()[0].Rule.ID) + } +} + +// TestRun_RegeneratesCleanly checks that run replaces contentDir's previous output rather than +// merely adding to it: a stale file left over from a since-removed rule must not survive a rerun. +func TestRun_RegeneratesCleanly(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + readmePath := filepath.Join(dir, "README.md") + if err := os.WriteFile(readmePath, []byte(fixtureReadme), 0o644); err != nil { + t.Fatalf("write fixture README: %v", err) + } + contentDir := filepath.Join(dir, "content") + + stale := filepath.Join(contentDir, "rules", "stale-rule.md") + if err := os.MkdirAll(filepath.Dir(stale), 0o755); err != nil { + t.Fatalf("create %s: %v", filepath.Dir(stale), err) + } + if err := os.WriteFile(stale, []byte("stale"), 0o644); err != nil { + t.Fatalf("write %s: %v", stale, err) + } + + if err := run(readmePath, contentDir); err != nil { + t.Fatalf("run: %v", err) + } + if _, err := os.Stat(stale); !os.IsNotExist(err) { + t.Errorf("run left %s behind, want it removed", stale) + } +} diff --git a/cmd/docgen/readme.go b/cmd/docgen/readme.go new file mode 100644 index 0000000..c72d02e --- /dev/null +++ b/cmd/docgen/readme.go @@ -0,0 +1,212 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" +) + +// readme.go turns README.md into the three top-level site pages that mirror it — the landing page, +// Getting started, and Reference — so their prose has exactly one source. Splitting it means +// rewriting the anchor links the README uses to jump within itself: an anchor now on a different +// page must point there instead ("reference.md#config-file"), which the goldmark link render hook +// enabled in hugo.toml resolves to that page's real address. + +// README section boundaries, matched as exact heading lines. A generator failing loudly when README +// is restructured is the right failure mode; silently misplacing content is not. +const ( + readmeWhyHeading = "## Why decolint" + readmeTryHeading = "## Try it" + readmeLintingHeading = "## Linting a Feature or Template" + readmeReferenceH1 = "# Reference" + readmeContribHeading = "## Contributing" + readmeHorizontalRule = "---" + landingDescription = "A linter for Dev Container configuration files." + gettingStartedSummary = "Install decolint, choose what it reports, and wire it into CI." + referenceSummary = "Every flag, config file member, and output format decolint supports." +) + +// writeReadmePages splits the README at readmePath into the landing, getting-started and reference +// pages and writes them under dir. +func writeReadmePages(readmePath, dir string) error { + src, err := os.ReadFile(readmePath) + if err != nil { + return fmt.Errorf("read %s: %w", readmePath, err) + } + pages, err := splitReadme(string(src)) + if err != nil { + return fmt.Errorf("%s: %w", readmePath, err) + } + for name, page := range pages { + path := filepath.Join(dir, name+".md") + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return fmt.Errorf("create %s: %w", filepath.Dir(path), err) + } + if err := os.WriteFile(path, []byte(page), 0o644); err != nil { + return fmt.Errorf("write %s: %w", path, err) + } + } + return nil +} + +// splitReadme splits README.md's content src into the landing, getting-started and reference +// pages, keyed by output file name (without extension), each a complete page: front matter, then +// body with cross-page anchors rewritten (see rewriteAnchors). +func splitReadme(src string) (map[string]string, error) { + lines := strings.Split(src, "\n") + + titleIdx := findTitleLine(lines) + whyIdx := findHeadingLine(lines, readmeWhyHeading) + tryIdx := findHeadingLine(lines, readmeTryHeading) + lintingIdx := findHeadingLine(lines, readmeLintingHeading) + refIdx := findHeadingLine(lines, readmeReferenceH1) + contribIdx := findHeadingLine(lines, readmeContribHeading) + if titleIdx < 0 || whyIdx < 0 || tryIdx < 0 || lintingIdx < 0 || refIdx < 0 || contribIdx < 0 { + return nil, fmt.Errorf("could not find a title and all expected section headings (title=%d why=%d try=%d linting=%d reference=%d contributing=%d)", + titleIdx, whyIdx, tryIdx, lintingIdx, refIdx, contribIdx) + } + if titleIdx >= whyIdx || whyIdx >= tryIdx || tryIdx >= lintingIdx || lintingIdx >= refIdx || refIdx >= contribIdx { + return nil, fmt.Errorf("the title and section headings are not in the expected order (title=%d why=%d try=%d linting=%d reference=%d contributing=%d)", + titleIdx, whyIdx, tryIdx, lintingIdx, refIdx, contribIdx) + } + + // Landing runs from the first content line after the title and badges through the end of "Why + // decolint"; getting-started picks up at "Try it" and runs to just before the "---" separator + // that precedes "# Reference"; reference runs from just after "# Reference" to just before + // "Contributing", which belongs to the project rather than to decolint's own documentation. + contentStart := skipTitleAndBadges(lines) + + bodies := map[string]string{ + "_index": strings.Join(trimBlank(lines[contentStart:tryIdx]), "\n"), + "getting-started": strings.Join(trimHorizontalRule(trimBlank(lines[tryIdx:refIdx])), "\n"), + "reference": strings.Join(stripRulesTableMarkers(trimBlank(lines[refIdx+1:contribIdx])), "\n"), + } + + // The heading -> page map spans all three bodies, so a link anywhere among them can be resolved + // to the page it actually lives on, or correctly left alone when it's already on the right one. + slugPage := map[string]string{} + for name, body := range bodies { + for _, h := range scanHeadings(body) { + slugPage[h.Slug] = name + } + } + + // Getting started and Reference are both long enough, with enough ## sections, to be worth a + // table of contents; see docs/layouts/baseof.html and page-toc.html. + frontMatter := map[string]string{ + "_index": "title: decolint\ndescription: " + yamlSingleQuoted(landingDescription), + "getting-started": "title: Getting started\ndescription: " + yamlSingleQuoted(gettingStartedSummary) + "\ntoc: true", + "reference": "title: Reference\ndescription: " + yamlSingleQuoted(referenceSummary) + "\ntoc: true", + } + + pages := make(map[string]string, len(bodies)) + for name, body := range bodies { + rewritten := rewriteAnchors(body, name, slugPage) + pages[name] = "---\n" + frontMatter[name] + "\n---\n\n" + rewritten + "\n" + } + return pages, nil +} + +// findHeadingLine returns the index of the line in lines that is exactly the ATX heading want +// (e.g. "## Why decolint"), or -1 if there is none. +func findHeadingLine(lines []string, want string) int { + for i, l := range lines { + if l == want { + return i + } + } + return -1 +} + +// findTitleLine returns the index of the first "# " (h1) line in lines, or -1 if there is none. +func findTitleLine(lines []string) int { + for i, l := range lines { + if strings.HasPrefix(l, "# ") { + return i + } + } + return -1 +} + +// skipTitleAndBadges returns the index of the first line of body prose: past the "# " title and any +// immediately following badge lines ("[![..."). +func skipTitleAndBadges(lines []string) int { + i := 0 + for i < len(lines) && !strings.HasPrefix(lines[i], "# ") { + i++ + } + i++ // past the title line itself + for i < len(lines) && (strings.TrimSpace(lines[i]) == "" || strings.HasPrefix(strings.TrimSpace(lines[i]), "[![")) { + i++ + } + return i +} + +// trimBlank drops leading and trailing blank lines from lines. +func trimBlank(lines []string) []string { + start := 0 + for start < len(lines) && strings.TrimSpace(lines[start]) == "" { + start++ + } + end := len(lines) + for end > start && strings.TrimSpace(lines[end-1]) == "" { + end-- + } + return lines[start:end] +} + +// stripRulesTableMarkers drops the rulesTableStart/rulesTableEnd lines: bookkeeping for +// updateReadmeRulesTable, meaningless once the table is on its own page rather than sitting in +// README.md. +func stripRulesTableMarkers(lines []string) []string { + out := make([]string, 0, len(lines)) + for _, l := range lines { + if l == rulesTableStart || l == rulesTableEnd { + continue + } + out = append(out, l) + } + return out +} + +// trimHorizontalRule drops a trailing "---" line (and the blank lines around it, already stripped +// by trimBlank) — the separator README.md draws between the guide and "# Reference", which has +// nothing to do with the guide's own last section. +func trimHorizontalRule(lines []string) []string { + if len(lines) > 0 && lines[len(lines)-1] == readmeHorizontalRule { + return trimBlank(lines[:len(lines)-1]) + } + return lines +} + +// anchorLink matches a Markdown link whose target is a same-document fragment, e.g. "(#config-file)". +var anchorLink = regexp.MustCompile(`\]\(#([a-z0-9-]+)\)`) + +// rewriteAnchors rewrites every "(#slug)" link in body that names a heading living on a different +// page than page, to "(.md#slug)", fence-aware so a "#" shown inside an example is never +// touched. A slug not found in slugPage (nothing in README.md's current content triggers this; see +// writeReadmePages) is left alone rather than guessed at. +func rewriteAnchors(body, page string, slugPage map[string]string) string { + lines := strings.Split(body, "\n") + fenced := false + for i, line := range lines { + if strings.HasPrefix(strings.TrimSpace(line), "```") { + fenced = !fenced + continue + } + if fenced { + continue + } + lines[i] = anchorLink.ReplaceAllStringFunc(line, func(m string) string { + slug := anchorLink.FindStringSubmatch(m)[1] + target, ok := slugPage[slug] + if !ok || target == page { + return m + } + return "](" + target + ".md#" + slug + ")" + }) + } + return strings.Join(lines, "\n") +} diff --git a/cmd/docgen/readme_test.go b/cmd/docgen/readme_test.go new file mode 100644 index 0000000..3880dab --- /dev/null +++ b/cmd/docgen/readme_test.go @@ -0,0 +1,150 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// fixtureReadme is a minimal README.md with the section structure splitReadme requires: the +// headings it splits on, a cross-page anchor link in each direction, and the rules table markers, +// so a single fixture exercises the heading split, anchor rewriting and marker stripping together. +const fixtureReadme = `# example + +[![CI](https://example.invalid/badge.svg)](https://example.invalid) + +Intro paragraph. See [Config file](#config-file). + +## Why decolint + +- A landing page bullet. + +## Try it + +Getting started body. Self link: [Try it](#try-it). + +## Linting a Feature or Template + +Last guide section. + +--- + +# Reference + +## What decolint lints + +Reference body. + +## Config file + + +| ID | +| --- | +| ` + "`x`" + ` | + + +## Contributing + +Not part of the site. +` + +func TestSplitReadme(t *testing.T) { + t.Parallel() + + pages, err := splitReadme(fixtureReadme) + if err != nil { + t.Fatalf("splitReadme: %v", err) + } + for _, name := range []string{"_index", "getting-started", "reference"} { + if _, ok := pages[name]; !ok { + t.Errorf("splitReadme result has no %q page", name) + } + } + + landing := pages["_index"] + if !strings.Contains(landing, "title: decolint") { + t.Errorf("_index front matter missing title, got:\n%s", landing) + } + if !strings.Contains(landing, "Intro paragraph.") || !strings.Contains(landing, "landing page bullet") { + t.Errorf("_index body missing expected content, got:\n%s", landing) + } + if strings.Contains(landing, "## Try it") { + t.Errorf("_index leaked getting-started content, got:\n%s", landing) + } + // "Config file" lives on reference, so the landing page's link to it must be rewritten. + if !strings.Contains(landing, "(reference.md#config-file)") { + t.Errorf("_index anchor to #config-file was not rewritten to reference.md, got:\n%s", landing) + } + + gs := pages["getting-started"] + if !strings.Contains(gs, "title: Getting started") || !strings.Contains(gs, "toc: true") { + t.Errorf("getting-started front matter missing title/toc, got:\n%s", gs) + } + if strings.Contains(gs, "Reference body") || strings.Contains(gs, "Not part of the site") { + t.Errorf("getting-started leaked reference/contributing content, got:\n%s", gs) + } + // "Try it" is on the same page, so this link must stay untouched. + if !strings.Contains(gs, "[Try it](#try-it)") { + t.Errorf("getting-started same-page anchor was rewritten, got:\n%s", gs) + } + gsBody := strings.SplitN(gs, "\n---\n\n", 2)[1] // past the front matter fence + if strings.Contains(gsBody, "---") { + t.Errorf("getting-started retained the horizontal rule before # Reference, got body:\n%s", gsBody) + } + + ref := pages["reference"] + if !strings.Contains(ref, "title: Reference") || !strings.Contains(ref, "toc: true") { + t.Errorf("reference front matter missing title/toc, got:\n%s", ref) + } + if strings.Contains(ref, "Not part of the site") { + t.Errorf("reference leaked Contributing content, got:\n%s", ref) + } + if strings.Contains(ref, rulesTableStart) || strings.Contains(ref, rulesTableEnd) { + t.Errorf("reference retained the rules table markers, got:\n%s", ref) + } + if !strings.Contains(ref, "| `x` |") { + t.Errorf("reference lost the table content between the markers, got:\n%s", ref) + } +} + +func TestSplitReadme_MissingHeading(t *testing.T) { + t.Parallel() + + _, err := splitReadme("# example\n\n## Try it\n\nno other headings\n") + if err == nil { + t.Fatal("splitReadme with missing headings: got nil error, want one") + } +} + +// TestSplitReadme_MissingTitle guards against a panic: with no "# " title above the sections, +// skipTitleAndBadges used to scan past the end of lines, and splitReadme sliced with that +// out-of-bounds index. +func TestSplitReadme_MissingTitle(t *testing.T) { + t.Parallel() + + untitled := strings.Replace(fixtureReadme, "# example\n", "", 1) + _, err := splitReadme(untitled) + if err == nil { + t.Fatal("splitReadme with no title: got nil error, want one") + } +} + +func TestWriteReadmePages(t *testing.T) { + t.Parallel() + + readmePath := filepath.Join(t.TempDir(), "README.md") + if err := os.WriteFile(readmePath, []byte(fixtureReadme), 0o644); err != nil { + t.Fatalf("write fixture README: %v", err) + } + dir := t.TempDir() + + if err := writeReadmePages(readmePath, dir); err != nil { + t.Fatalf("writeReadmePages: %v", err) + } + for _, name := range []string{"_index.md", "getting-started.md", "reference.md"} { + if _, err := os.Stat(filepath.Join(dir, name)); err != nil { + t.Errorf("writeReadmePages did not create %s: %v", name, err) + } + } +} diff --git a/cmd/docgen/rules.go b/cmd/docgen/rules.go new file mode 100644 index 0000000..d355e4a --- /dev/null +++ b/cmd/docgen/rules.go @@ -0,0 +1,177 @@ +package main + +import ( + "cmp" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/bare-devcontainer/decolint/linter" + "github.com/bare-devcontainer/decolint/rules" +) + +// writeRulePages renders one Markdown page per built-in rule into dir/rules, named after the rule's +// ID. It is the site's only source for the rule reference: nothing under docs/content/rules other +// than _index.md is committed, so a rule's documentation cannot drift from rules/*.go. +func writeRulePages(dir string) error { + rulesDir := filepath.Join(dir, "rules") + if err := os.MkdirAll(rulesDir, 0o755); err != nil { + return fmt.Errorf("create %s: %w", rulesDir, err) + } + for _, reg := range rules.Builtin() { + page := renderRulePage(reg.Rule) + path := filepath.Join(rulesDir, reg.Rule.ID+".md") + if err := os.WriteFile(path, []byte(page), 0o644); err != nil { + return fmt.Errorf("write %s: %w", path, err) + } + } + return nil +} + +// renderRulePage renders one rule's documentation page: front matter matching what +// docs/layouts/page.html reads, the rationale, a Bad and a Good example, and its references. +func renderRulePage(r *linter.Rule) string { + var b strings.Builder + + fmt.Fprintf(&b, "---\n") + fmt.Fprintf(&b, "title: %s\n", r.ID) + fmt.Fprintf(&b, "category: %s\n", r.Category) + fmt.Fprintf(&b, "platforms: [%s]\n", strings.Join(platformNames(r.Platforms), ", ")) + fmt.Fprintf(&b, "file_types: [%s]\n", strings.Join(fileTypeNames(r.FileTypes), ", ")) + fmt.Fprintf(&b, "description: %s\n", yamlSingleQuoted(r.Description)) + fmt.Fprintf(&b, "---\n\n") + + fmt.Fprintf(&b, "## Why\n\n%s\n\n", r.LongDescription) + + fmt.Fprintf(&b, "## Bad\n\n%s\n", strings.TrimRight(renderSnippet(r.Example.Bad), "\n")) + fmt.Fprintf(&b, "\n## Good\n\n%s\n", strings.TrimRight(renderSnippet(r.Example.Good), "\n")) + if r.Example.Note != "" { + fmt.Fprintf(&b, "\n%s\n", r.Example.Note) + } + + fmt.Fprintf(&b, "\n## References\n\n") + for _, ref := range r.References { + fmt.Fprintf(&b, "- <%s>\n", ref) + } + + return b.String() +} + +// renderSnippet renders a [linter.Snippet] as one fenced code block per file, each preceded by a +// "### `path`" heading when the snippet has more than one file, or when a non-default Mode is the +// only thing distinguishing the file from its counterpart in the other half of the example (e.g. an +// install.sh whose content is identical in Bad and Good). +func renderSnippet(s linter.Snippet) string { + var b strings.Builder + for _, f := range s.Files { + switch { + case f.Mode != 0: + fmt.Fprintf(&b, "### `%s` (mode %04o)\n\n", f.Path, f.Mode) + case len(s.Files) > 1: + fmt.Fprintf(&b, "### `%s`\n\n", f.Path) + } + fmt.Fprintf(&b, "```%s\n%s```\n\n", codeLang(f.Path), f.Content) + } + return b.String() +} + +// codeLang returns the fenced-block language for a file named path, guessed from its extension. +func codeLang(path string) string { + switch { + case strings.HasSuffix(path, ".json"): + // The examples use // comments for context, so they need JSONC's lexer, not plain JSON's. + return "jsonc" + case strings.HasSuffix(path, ".sh"): + return "bash" + default: + return "text" + } +} + +func platformNames(platforms []linter.Platform) []string { + names := make([]string, len(platforms)) + for i, p := range platforms { + names[i] = p.String() + } + return names +} + +func fileTypeNames(fileTypes []linter.FileType) []string { + names := make([]string, len(fileTypes)) + for i, ft := range fileTypes { + names[i] = string(ft) + } + return names +} + +// yamlSingleQuoted renders s as a single-quoted YAML scalar: the one quoting style that needs no +// escaping for the double quotes a rule's Description is full of ("image", "build", ...); a literal +// single quote doubles, per YAML's own rule for this style. +func yamlSingleQuoted(s string) string { + return "'" + strings.ReplaceAll(s, "'", "''") + "'" +} + +// rulesTableMarkers delimit the generated rules table in README.md, so updateReadmeRulesTable can +// find and replace exactly that table and nothing else. +const ( + rulesTableStart = "" + rulesTableEnd = "" +) + +// updateReadmeRulesTable rewrites the table between the rulesTableStart/rulesTableEnd markers in +// the README at path to the current built-in rules, sorted by category (in the same order the +// category list above the table uses) then ID. It is an error if the markers are missing. +func updateReadmeRulesTable(path string) error { + src, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("read %s: %w", path, err) + } + + start := strings.Index(string(src), rulesTableStart) + end := strings.Index(string(src), rulesTableEnd) + if start < 0 || end < 0 || end < start { + return fmt.Errorf("%s: rules table markers %q/%q not found", path, rulesTableStart, rulesTableEnd) + } + end += len(rulesTableEnd) + + table := rulesTableStart + "\n" + renderRulesTable() + rulesTableEnd + out := string(src[:start]) + table + string(src[end:]) + if out == string(src) { + return nil + } + if err := os.WriteFile(path, []byte(out), 0o644); err != nil { + return fmt.Errorf("write %s: %w", path, err) + } + return nil +} + +// renderRulesTable renders every built-in rule as a Markdown table row, each ID linking to its +// documentation page, sorted by category then ID. +func renderRulesTable() string { + regs := slices.Clone(rules.Builtin()) + slices.SortFunc(regs, func(a, b rules.Registration) int { + if c := cmp.Compare(a.Rule.Category, b.Rule.Category); c != 0 { + return c + } + return cmp.Compare(a.Rule.ID, b.Rule.ID) + }) + + var b strings.Builder + b.WriteString("| ID | Category | Platform | Description |\n") + b.WriteString("| --- | --- | --- | --- |\n") + for _, reg := range regs { + r := reg.Rule + platform := "(all)" + if names := platformNames(r.Platforms); len(names) > 0 { + quoted := make([]string, len(names)) + for i, n := range names { + quoted[i] = "`" + n + "`" + } + platform = strings.Join(quoted, ", ") + } + fmt.Fprintf(&b, "| [`%s`](%s) | `%s` | %s | %s |\n", r.ID, rules.DocsURL(r.ID), r.Category, platform, r.Description) + } + return b.String() +} diff --git a/cmd/docgen/rules_test.go b/cmd/docgen/rules_test.go new file mode 100644 index 0000000..a2199e1 --- /dev/null +++ b/cmd/docgen/rules_test.go @@ -0,0 +1,249 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/bare-devcontainer/decolint/linter" + "github.com/bare-devcontainer/decolint/rules" +) + +func TestYamlSingleQuoted(t *testing.T) { + t.Parallel() + + tests := []struct{ in, want string }{ + {`disallow "privileged"`, `'disallow "privileged"'`}, + {`it's fine`, `'it''s fine'`}, + {"plain", "'plain'"}, + } + for _, tt := range tests { + if got := yamlSingleQuoted(tt.in); got != tt.want { + t.Errorf("yamlSingleQuoted(%q) = %q, want %q", tt.in, got, tt.want) + } + } +} + +func TestCodeLang(t *testing.T) { + t.Parallel() + + tests := []struct{ path, want string }{ + {"devcontainer.json", "jsonc"}, + {"devcontainer-feature.json", "jsonc"}, + {"install.sh", "bash"}, + {"README.md", "text"}, + } + for _, tt := range tests { + if got := codeLang(tt.path); got != tt.want { + t.Errorf("codeLang(%q) = %q, want %q", tt.path, got, tt.want) + } + } +} + +func TestRenderRulePage(t *testing.T) { + t.Parallel() + + r := &linter.Rule{ + ID: "my-rule", + Description: `disallow "foo"`, + LongDescription: "Why it matters.", + References: []string{"https://example.invalid/a", "https://example.invalid/b"}, + Category: linter.CategorySecurity, + FileTypes: []linter.FileType{linter.Devcontainer}, + Platforms: []linter.Platform{linter.PlatformCodespaces}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{{Path: "devcontainer.json", Content: "{\n \"privileged\": true\n}\n"}}, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{{Path: "devcontainer.json", Content: "{}\n"}}, + }, + Note: "A closing note.", + }, + } + + page := renderRulePage(r) + + for _, want := range []string{ + "title: my-rule", + "category: security", + "platforms: [codespaces]", + "file_types: [devcontainer]", + `description: 'disallow "foo"'`, + "## Why\n\nWhy it matters.", + "## Bad", + `"privileged": true`, + "## Good", + "A closing note.", + "## References", + "- ", + "- ", + } { + if !strings.Contains(page, want) { + t.Errorf("renderRulePage() missing %q, got:\n%s", want, page) + } + } +} + +func TestRenderRulePage_MultiFileExample(t *testing.T) { + t.Parallel() + + r := &linter.Rule{ + ID: "multi-file-rule", + Description: "d", + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Template}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: "devcontainer-template.json", Content: "{}\n"}, + {Path: ".devcontainer/devcontainer.json", Content: "{}\n"}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: "devcontainer-template.json", Content: "{}\n"}, + {Path: ".devcontainer/devcontainer.json", Content: "{}\n"}, + }, + }, + }, + } + + page := renderRulePage(r) + for _, want := range []string{ + "### `devcontainer-template.json`", + "### `.devcontainer/devcontainer.json`", + } { + if !strings.Contains(page, want) { + t.Errorf("renderRulePage() missing %q for a multi-file example, got:\n%s", want, page) + } + } +} + +func TestRenderRulePage_ModeCaption(t *testing.T) { + t.Parallel() + + r := &linter.Rule{ + ID: "mode-rule", + Description: "d", + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Feature}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{{Path: "install.sh", Content: "#!/bin/sh\n", Mode: 0o644}}, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{{Path: "install.sh", Content: "#!/bin/sh\n", Mode: 0o755}}, + }, + }, + } + + page := renderRulePage(r) + if !strings.Contains(page, "(mode 0644)") { + t.Errorf("renderRulePage() missing the Bad file's mode caption, got:\n%s", page) + } + if !strings.Contains(page, "(mode 0755)") { + t.Errorf("renderRulePage() missing the Good file's mode caption, got:\n%s", page) + } +} + +func TestWriteRulePages(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + if err := writeRulePages(dir); err != nil { + t.Fatalf("writeRulePages: %v", err) + } + + builtin := rules.Builtin() + entries, err := os.ReadDir(filepath.Join(dir, "rules")) + if err != nil { + t.Fatalf("read rules dir: %v", err) + } + if len(entries) != len(builtin) { + t.Errorf("writeRulePages wrote %d file(s), want %d (one per built-in rule)", len(entries), len(builtin)) + } + + path := filepath.Join(dir, "rules", "no-image-latest.md") + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + if !strings.Contains(string(data), "title: no-image-latest") { + t.Errorf("%s missing expected title, got:\n%s", path, data) + } +} + +func TestRenderRulesTable(t *testing.T) { + t.Parallel() + + table := renderRulesTable() + if !strings.HasPrefix(table, "| ID | Category | Platform | Description |\n") { + t.Fatalf("renderRulesTable() does not start with the header row, got:\n%s", table) + } + + rows := strings.Split(strings.TrimRight(table, "\n"), "\n") + if want := len(rules.Builtin()) + 2; len(rows) != want { // +2 for the header and separator rows + t.Errorf("renderRulesTable() produced %d row(s), want %d", len(rows), want) + } + + // Sorted by category (Correctness < Security < ... per the linter.Category iota order), then ID: + // the first data row must be a correctness rule, and the table must link to rules.DocsURL. + if !strings.Contains(rows[2], "`correctness`") { + t.Errorf("first data row is not a correctness rule, got: %s", rows[2]) + } + if !strings.Contains(table, rules.DocsURL("no-image-latest")) { + t.Errorf("renderRulesTable() does not link to rules.DocsURL, got:\n%s", table) + } +} + +func TestUpdateReadmeRulesTable(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "README.md") + original := "prose\n\n" + rulesTableStart + "\nstale\n" + rulesTableEnd + "\n\nmore prose\n" + if err := os.WriteFile(path, []byte(original), 0o644); err != nil { + t.Fatalf("write fixture: %v", err) + } + + if err := updateReadmeRulesTable(path); err != nil { + t.Fatalf("updateReadmeRulesTable: %v", err) + } + + got, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + if strings.Contains(string(got), "stale") { + t.Errorf("updateReadmeRulesTable did not replace the stale table, got:\n%s", got) + } + if !strings.HasPrefix(string(got), "prose\n\n"+rulesTableStart) || !strings.HasSuffix(string(got), "\n\nmore prose\n") { + t.Errorf("updateReadmeRulesTable disturbed content outside the markers, got:\n%s", got) + } + + // Running it again on its own output must be a no-op (the generator has to be idempotent, since + // CI fails the build if it finds anything left to regenerate). + if err := updateReadmeRulesTable(path); err != nil { + t.Fatalf("updateReadmeRulesTable (second run): %v", err) + } + got2, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + if string(got2) != string(got) { + t.Errorf("updateReadmeRulesTable is not idempotent:\nfirst:\n%s\nsecond:\n%s", got, got2) + } +} + +func TestUpdateReadmeRulesTable_MissingMarkers(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "README.md") + if err := os.WriteFile(path, []byte("no markers here\n"), 0o644); err != nil { + t.Fatalf("write fixture: %v", err) + } + if err := updateReadmeRulesTable(path); err == nil { + t.Fatal("updateReadmeRulesTable with no markers: got nil error, want one") + } +} diff --git a/cmd/docgen/slug.go b/cmd/docgen/slug.go new file mode 100644 index 0000000..bbfd1bc --- /dev/null +++ b/cmd/docgen/slug.go @@ -0,0 +1,70 @@ +package main + +import ( + "regexp" + "strings" +) + +// slugify computes a Markdown heading's anchor slug the way GitHub (and Hugo's default goldmark +// auto-heading-id extension) does: lowercase, drop everything but letters, digits, spaces and +// hyphens, then turn runs of spaces/hyphens into one hyphen. Verified against every heading in +// README.md to match the anchors already written by hand there. +func slugify(heading string) string { + var b strings.Builder + for _, r := range strings.ToLower(heading) { + switch { + case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == ' ', r == '-': + b.WriteRune(r) + } + } + return strings.Trim(collapseHyphens.ReplaceAllString(b.String(), "-"), "-") +} + +var collapseHyphens = regexp.MustCompile(`[\s-]+`) + +// heading is one ATX heading found by scanHeadings: its level (1 for "#", 2 for "##", ...), text, +// and computed slug. +type heading struct { + Level int + Text string + Slug string +} + +// scanHeadings returns every ATX heading ("# ", "## ", ...) in body, skipping fenced code blocks so +// a "#" inside an example is never mistaken for one. README.md has none today, but a generator that +// assumed so would fail silently the day it does. +func scanHeadings(body string) []heading { + var out []heading + fenced := false + for line := range strings.SplitSeq(body, "\n") { + if strings.HasPrefix(strings.TrimSpace(line), "```") { + fenced = !fenced + continue + } + if fenced { + continue + } + level, text, ok := atxHeading(line) + if !ok { + continue + } + out = append(out, heading{Level: level, Text: text, Slug: slugify(text)}) + } + return out +} + +// atxHeading parses line as an ATX heading ("# Title" through "###### Title"), returning its level +// and text with any trailing "#"s trimmed. +func atxHeading(line string) (level int, text string, ok bool) { + for level < len(line) && level < 6 && line[level] == '#' { + level++ + } + if level == 0 || level >= len(line) || line[level] != ' ' { + return 0, "", false + } + text = strings.TrimSpace(strings.TrimRight(line[level+1:], "#")) + if text == "" { + return 0, "", false + } + return level, text, true +} diff --git a/cmd/docgen/slug_test.go b/cmd/docgen/slug_test.go new file mode 100644 index 0000000..d3fd200 --- /dev/null +++ b/cmd/docgen/slug_test.go @@ -0,0 +1,64 @@ +package main + +import "testing" + +func TestSlugify(t *testing.T) { + t.Parallel() + + tests := []struct { + heading string + want string + }{ + {"Why decolint", "why-decolint"}, + {"1. Run it", "1-run-it"}, + {"4. Lint what actually runs", "4-lint-what-actually-runs"}, + {"Prebuilt binary (recommended)", "prebuilt-binary-recommended"}, + {"Config file", "config-file"}, + {" Extra spaces ", "extra-spaces"}, + } + for _, tt := range tests { + if got := slugify(tt.heading); got != tt.want { + t.Errorf("slugify(%q) = %q, want %q", tt.heading, got, tt.want) + } + } +} + +func TestScanHeadings_SkipsFencedCode(t *testing.T) { + t.Parallel() + + body := "## Real heading\n\n```console\n# not a heading\n## also not one\n```\n\n## Another\n" + got := scanHeadings(body) + if len(got) != 2 { + t.Fatalf("scanHeadings found %d heading(s), want 2: %+v", len(got), got) + } + if got[0].Text != "Real heading" || got[1].Text != "Another" { + t.Errorf("scanHeadings = %+v", got) + } +} + +func TestRewriteAnchors(t *testing.T) { + t.Parallel() + + slugPage := map[string]string{ + "local": "getting-started", + "remote": "reference", + } + body := "See [here](#local) and [there](#remote), plus [unknown](#nowhere)." + got := rewriteAnchors(body, "getting-started", slugPage) + want := "See [here](#local) and [there](reference.md#remote), plus [unknown](#nowhere)." + if got != want { + t.Errorf("rewriteAnchors() = %q, want %q", got, want) + } +} + +func TestRewriteAnchors_SkipsFencedCode(t *testing.T) { + t.Parallel() + + slugPage := map[string]string{"remote": "reference"} + body := "text [a](#remote)\n\n```\n[b](#remote)\n```\n" + got := rewriteAnchors(body, "getting-started", slugPage) + want := "text [a](reference.md#remote)\n\n```\n[b](#remote)\n```\n" + if got != want { + t.Errorf("rewriteAnchors() = %q, want %q", got, want) + } +} diff --git a/docs/assets/css/main.css b/docs/assets/css/main.css new file mode 100644 index 0000000..e99a330 --- /dev/null +++ b/docs/assets/css/main.css @@ -0,0 +1,300 @@ +/* decolint documentation site. + Colors are declared once as custom properties and re-declared for a dark preference; the + Chroma token colors generated by `make site-syntax` live in syntax.css. */ + +:root { + color-scheme: light dark; + --bg: #ffffff; + --fg: #1f2328; + --fg-muted: #59636e; + --border: #d1d9e0; + --accent: #0969da; + --surface: #f6f8fa; + --content-width: 58rem; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0d1117; + --fg: #e6edf3; + --fg-muted: #9198a1; + --border: #3d444d; + --accent: #4493f8; + --surface: #151b23; + } +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--bg); + color: var(--fg); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 1.6; +} + +a { + color: var(--accent); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +code, +pre { + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; + font-size: 0.875em; +} + +code { + background: var(--surface); + border-radius: 4px; + padding: 0.15em 0.35em; +} + +pre code { + background: none; + padding: 0; +} + +.highlight pre, +pre { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + overflow-x: auto; + padding: 0.85rem 1rem; +} + +table { + border-collapse: collapse; + display: block; + overflow-x: auto; + width: 100%; +} + +th, +td { + border: 1px solid var(--border); + padding: 0.4rem 0.7rem; + text-align: left; + vertical-align: top; +} + +th { + background: var(--surface); +} + +/* Header, footer */ + +.site-header { + align-items: center; + border-bottom: 1px solid var(--border); + display: flex; + flex-wrap: wrap; + gap: 1.25rem; + padding: 0.9rem 1.5rem; +} + +.site-title { + display: inline-flex; +} + +/* The logo's wordmark already renders "decolint", in a color chosen for one background; swap + which variant is visible instead of recoloring, matching how the rest of the site's colors + respond to prefers-color-scheme. */ +.site-logo { + display: block; + height: 28px; + width: auto; +} + +.site-logo--dark { + display: none; +} + +@media (prefers-color-scheme: dark) { + .site-logo--light { + display: none; + } + + .site-logo--dark { + display: block; + } +} + +.site-header nav { + display: flex; + gap: 1.25rem; +} + +.site-header__github { + margin-left: auto; +} + +.site-header nav a[aria-current="page"] { + color: var(--fg); + font-weight: 600; +} + +.site-footer { + border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent); + color: var(--fg-muted); + font-size: 0.9rem; + margin-top: 4rem; + padding: 1.5rem; + text-align: center; +} + +.site-footer p { + margin: 0 0 0.35rem; +} + +/* Layout */ + +.layout { + margin: 0 auto; + max-width: var(--content-width); + padding: 1.5rem; +} + +.layout--with-sidebar { + display: grid; + gap: 2.5rem; + grid-template-columns: 15rem minmax(0, var(--content-width)); + max-width: calc(var(--content-width) + 17.5rem); +} + +main { + min-width: 0; +} + +h1 { + font-size: 1.9rem; + line-height: 1.25; + margin: 0.5rem 0 1rem; +} + +h2 { + border-bottom: 1px solid var(--border); + font-size: 1.3rem; + margin: 2.2rem 0 0.9rem; + padding-bottom: 0.3rem; +} + +h3 { + font-size: 1rem; + margin: 1.5rem 0 0.5rem; +} + +/* Rule pages */ + +.rule-summary { + color: var(--fg-muted); + font-size: 1.05rem; + margin-top: 0; +} + +.rule-meta { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + display: grid; + gap: 0.15rem 1rem; + grid-template-columns: auto 1fr; + margin: 0 0 1.5rem; + padding: 0.8rem 1rem; +} + +.rule-meta dt { + color: var(--fg-muted); + font-size: 0.85rem; +} + +.rule-meta dd { + margin: 0; + font-size: 0.85rem; +} + +.rule-nav, +.page-toc { + align-self: start; + border-right: 1px solid var(--border); + font-size: 0.9rem; + max-height: calc(100vh - 3rem); + overflow-y: auto; + padding-right: 1.25rem; + position: sticky; + top: 1.5rem; +} + +.rule-nav__all { + font-weight: 600; +} + +.rule-nav h2, +.page-toc__label { + color: var(--fg-muted); + font-size: 0.75rem; + letter-spacing: 0.04em; + margin: 1.4rem 0 0.4rem; + text-transform: uppercase; +} + +.rule-nav h2 { + border: 0; + padding: 0; +} + +.page-toc__label { + margin-top: 0; +} + +.rule-nav ul, +.page-toc ul { + list-style: none; + margin: 0; + padding: 0; +} + +/* Nested (h3-under-h2) entries in the table of contents. */ +.page-toc ul ul { + padding-left: 0.9rem; +} + +.rule-nav li, +.page-toc li { + margin: 0.15rem 0; +} + +.rule-nav a, +.page-toc a { + word-break: break-word; +} + +.rule-nav a[aria-current="page"] { + color: var(--fg); + font-weight: 600; +} + +@media (max-width: 60rem) { + .layout--with-sidebar { + grid-template-columns: minmax(0, 1fr); + } + + .rule-nav, + .page-toc { + border-bottom: 1px solid var(--border); + border-right: 0; + max-height: none; + overflow-y: visible; + padding: 0 0 1rem; + position: static; + } +} diff --git a/docs/assets/css/syntax.css b/docs/assets/css/syntax.css new file mode 100644 index 0000000..f3045a6 --- /dev/null +++ b/docs/assets/css/syntax.css @@ -0,0 +1,233 @@ +/* Chroma syntax highlighting token colors. + Generated by `make site-syntax`; edit that target rather than this file. */ + + +/* Background */ .bg { background-color:#f7f7f7; } +/* PreWrapper */ .chroma { background-color:#f7f7f7;-webkit-text-size-adjust:none; } +/* Error */ .chroma .err { color:#f6f8fa;background-color:#82071e } +/* LineLink */ .chroma .lnlinks { outline:none;text-decoration:none;color:inherit } +/* LineTableTD */ .chroma .lntd { vertical-align:top;padding:0;margin:0;border:0; } +/* LineTable */ .chroma .lntable { border-spacing:0;padding:0;margin:0;border:0; } +/* LineHighlight */ .chroma .hl { background-color:#dedede } +/* LineNumbersTable */ .chroma .lnt { white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f } +/* LineNumbers */ .chroma .ln { white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f } +/* Line */ .chroma .line { display:flex; } +/* Keyword */ .chroma .k { color:#cf222e } +/* KeywordConstant */ .chroma .kc { color:#cf222e } +/* KeywordDeclaration */ .chroma .kd { color:#cf222e } +/* KeywordNamespace */ .chroma .kn { color:#cf222e } +/* KeywordPseudo */ .chroma .kp { color:#cf222e } +/* KeywordReserved */ .chroma .kr { color:#cf222e } +/* KeywordType */ .chroma .kt { color:#cf222e } +/* NameAttribute */ .chroma .na { color:#1f2328 } +/* NameClass */ .chroma .nc { color:#1f2328 } +/* NameConstant */ .chroma .no { color:#0550ae } +/* NameDecorator */ .chroma .nd { color:#0550ae } +/* NameEntity */ .chroma .ni { color:#6639ba } +/* NameLabel */ .chroma .nl { color:#900;font-weight:bold } +/* NameNamespace */ .chroma .nn { color:#24292e } +/* NameOther */ .chroma .nx { color:#1f2328 } +/* NameTag */ .chroma .nt { color:#0550ae } +/* NameBuiltin */ .chroma .nb { color:#6639ba } +/* NameBuiltinPseudo */ .chroma .bp { color:#6a737d } +/* NameVariable */ .chroma .nv { color:#953800 } +/* NameVariableClass */ .chroma .vc { color:#953800 } +/* NameVariableGlobal */ .chroma .vg { color:#953800 } +/* NameVariableInstance */ .chroma .vi { color:#953800 } +/* NameVariableMagic */ .chroma .vm { color:#953800 } +/* NameFunction */ .chroma .nf { color:#6639ba } +/* NameFunctionMagic */ .chroma .fm { color:#6639ba } +/* LiteralString */ .chroma .s { color:#0a3069 } +/* LiteralStringAffix */ .chroma .sa { color:#0a3069 } +/* LiteralStringBacktick */ .chroma .sb { color:#0a3069 } +/* LiteralStringChar */ .chroma .sc { color:#0a3069 } +/* LiteralStringDelimiter */ .chroma .dl { color:#0a3069 } +/* LiteralStringDoc */ .chroma .sd { color:#0a3069 } +/* LiteralStringDouble */ .chroma .s2 { color:#0a3069 } +/* LiteralStringEscape */ .chroma .se { color:#0a3069 } +/* LiteralStringHeredoc */ .chroma .sh { color:#0a3069 } +/* LiteralStringInterpol */ .chroma .si { color:#0a3069 } +/* LiteralStringOther */ .chroma .sx { color:#0a3069 } +/* LiteralStringRegex */ .chroma .sr { color:#0a3069 } +/* LiteralStringSingle */ .chroma .s1 { color:#0a3069 } +/* LiteralStringSymbol */ .chroma .ss { color:#032f62 } +/* LiteralNumber */ .chroma .m { color:#0550ae } +/* LiteralNumberBin */ .chroma .mb { color:#0550ae } +/* LiteralNumberFloat */ .chroma .mf { color:#0550ae } +/* LiteralNumberHex */ .chroma .mh { color:#0550ae } +/* LiteralNumberInteger */ .chroma .mi { color:#0550ae } +/* LiteralNumberIntegerLong */ .chroma .il { color:#0550ae } +/* LiteralNumberOct */ .chroma .mo { color:#0550ae } +/* Operator */ .chroma .o { color:#0550ae } +/* OperatorWord */ .chroma .ow { color:#0550ae } +/* OperatorReserved */ .chroma .or { color:#0550ae } +/* Punctuation */ .chroma .p { color:#1f2328 } +/* Comment */ .chroma .c { color:#57606a } +/* CommentHashbang */ .chroma .ch { color:#57606a } +/* CommentMultiline */ .chroma .cm { color:#57606a } +/* CommentSingle */ .chroma .c1 { color:#57606a } +/* CommentSpecial */ .chroma .cs { color:#57606a } +/* CommentPreproc */ .chroma .cp { color:#57606a } +/* CommentPreprocFile */ .chroma .cpf { color:#57606a } +/* GenericDeleted */ .chroma .gd { color:#82071e;background-color:#ffebe9 } +/* GenericEmph */ .chroma .ge { color:#1f2328 } +/* GenericInserted */ .chroma .gi { color:#116329;background-color:#dafbe1 } +/* GenericOutput */ .chroma .go { color:#1f2328 } +/* GenericUnderline */ .chroma .gl { text-decoration:underline } +/* TextWhitespace */ .chroma .w { color:#fff } +@media (prefers-color-scheme: dark) { + /* github-dark omits token classes it colors the same as the default text color, + so fall back to inheriting that color instead of the light style's, which + stays in effect below for every class the dark style does define. */ + .chroma .bp { color: inherit } + .chroma .c { color: inherit } + .chroma .c1 { color: inherit } + .chroma .ch { color: inherit } + .chroma .cm { color: inherit } + .chroma .cp { color: inherit } + .chroma .cpf { color: inherit } + .chroma .cs { color: inherit } + .chroma .dl { color: inherit } + .chroma .err { color: inherit } + .chroma .fm { color: inherit } + .chroma .gd { color: inherit } + .chroma .ge { color: inherit } + .chroma .gi { color: inherit } + .chroma .gl { color: inherit } + .chroma .go { color: inherit } + .chroma .hl { color: inherit } + .chroma .il { color: inherit } + .chroma .k { color: inherit } + .chroma .kc { color: inherit } + .chroma .kd { color: inherit } + .chroma .kn { color: inherit } + .chroma .kp { color: inherit } + .chroma .kr { color: inherit } + .chroma .kt { color: inherit } + .chroma .line { color: inherit } + .chroma .ln { color: inherit } + .chroma .lnlinks { color: inherit } + .chroma .lnt { color: inherit } + .chroma .lntable { color: inherit } + .chroma .lntd { color: inherit } + .chroma .m { color: inherit } + .chroma .mb { color: inherit } + .chroma .mf { color: inherit } + .chroma .mh { color: inherit } + .chroma .mi { color: inherit } + .chroma .mo { color: inherit } + .chroma .na { color: inherit } + .chroma .nb { color: inherit } + .chroma .nc { color: inherit } + .chroma .nd { color: inherit } + .chroma .nf { color: inherit } + .chroma .ni { color: inherit } + .chroma .nl { color: inherit } + .chroma .nn { color: inherit } + .chroma .no { color: inherit } + .chroma .nt { color: inherit } + .chroma .nv { color: inherit } + .chroma .nx { color: inherit } + .chroma .o { color: inherit } + .chroma .or { color: inherit } + .chroma .ow { color: inherit } + .chroma .p { color: inherit } + .chroma .s { color: inherit } + .chroma .s1 { color: inherit } + .chroma .s2 { color: inherit } + .chroma .sa { color: inherit } + .chroma .sb { color: inherit } + .chroma .sc { color: inherit } + .chroma .sd { color: inherit } + .chroma .se { color: inherit } + .chroma .sh { color: inherit } + .chroma .si { color: inherit } + .chroma .sr { color: inherit } + .chroma .ss { color: inherit } + .chroma .sx { color: inherit } + .chroma .vc { color: inherit } + .chroma .vg { color: inherit } + .chroma .vi { color: inherit } + .chroma .vm { color: inherit } + .chroma .w { color: inherit } + + /* Background */ .bg { color:#e6edf3;background-color:#0d1117; } + /* PreWrapper */ .chroma { color:#e6edf3;background-color:#0d1117;-webkit-text-size-adjust:none; } + /* Error */ .chroma .err { color:#f85149 } + /* LineLink */ .chroma .lnlinks { outline:none;text-decoration:none;color:inherit } + /* LineTableTD */ .chroma .lntd { vertical-align:top;padding:0;margin:0;border:0; } + /* LineTable */ .chroma .lntable { border-spacing:0;padding:0;margin:0;border:0; } + /* LineHighlight */ .chroma .hl { background-color:#6e7681 } + /* LineNumbersTable */ .chroma .lnt { white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679 } + /* LineNumbers */ .chroma .ln { white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681 } + /* Line */ .chroma .line { display:flex; } + /* Keyword */ .chroma .k { color:#ff7b72 } + /* KeywordConstant */ .chroma .kc { color:#79c0ff } + /* KeywordDeclaration */ .chroma .kd { color:#ff7b72 } + /* KeywordNamespace */ .chroma .kn { color:#ff7b72 } + /* KeywordPseudo */ .chroma .kp { color:#79c0ff } + /* KeywordReserved */ .chroma .kr { color:#ff7b72 } + /* KeywordType */ .chroma .kt { color:#ff7b72 } + /* NameClass */ .chroma .nc { color:#f0883e;font-weight:bold } + /* NameConstant */ .chroma .no { color:#79c0ff;font-weight:bold } + /* NameDecorator */ .chroma .nd { color:#d2a8ff;font-weight:bold } + /* NameEntity */ .chroma .ni { color:#ffa657 } + /* NameException */ .chroma .ne { color:#f0883e;font-weight:bold } + /* NameLabel */ .chroma .nl { color:#79c0ff;font-weight:bold } + /* NameNamespace */ .chroma .nn { color:#ff7b72 } + /* NameOther */ .chroma .nx { color:#e6edf3 } + /* NameProperty */ .chroma .py { color:#79c0ff } + /* NameTag */ .chroma .nt { color:#7ee787 } + /* NameVariable */ .chroma .nv { color:#79c0ff } + /* NameVariableClass */ .chroma .vc { color:#79c0ff } + /* NameVariableGlobal */ .chroma .vg { color:#79c0ff } + /* NameVariableInstance */ .chroma .vi { color:#79c0ff } + /* NameVariableMagic */ .chroma .vm { color:#79c0ff } + /* NameFunction */ .chroma .nf { color:#d2a8ff;font-weight:bold } + /* NameFunctionMagic */ .chroma .fm { color:#d2a8ff;font-weight:bold } + /* Literal */ .chroma .l { color:#a5d6ff } + /* LiteralDate */ .chroma .ld { color:#79c0ff } + /* LiteralString */ .chroma .s { color:#a5d6ff } + /* LiteralStringAffix */ .chroma .sa { color:#79c0ff } + /* LiteralStringBacktick */ .chroma .sb { color:#a5d6ff } + /* LiteralStringChar */ .chroma .sc { color:#a5d6ff } + /* LiteralStringDelimiter */ .chroma .dl { color:#79c0ff } + /* LiteralStringDoc */ .chroma .sd { color:#a5d6ff } + /* LiteralStringDouble */ .chroma .s2 { color:#a5d6ff } + /* LiteralStringEscape */ .chroma .se { color:#79c0ff } + /* LiteralStringHeredoc */ .chroma .sh { color:#79c0ff } + /* LiteralStringInterpol */ .chroma .si { color:#a5d6ff } + /* LiteralStringOther */ .chroma .sx { color:#a5d6ff } + /* LiteralStringRegex */ .chroma .sr { color:#79c0ff } + /* LiteralStringSingle */ .chroma .s1 { color:#a5d6ff } + /* LiteralStringSymbol */ .chroma .ss { color:#a5d6ff } + /* LiteralNumber */ .chroma .m { color:#a5d6ff } + /* LiteralNumberBin */ .chroma .mb { color:#a5d6ff } + /* LiteralNumberFloat */ .chroma .mf { color:#a5d6ff } + /* LiteralNumberHex */ .chroma .mh { color:#a5d6ff } + /* LiteralNumberInteger */ .chroma .mi { color:#a5d6ff } + /* LiteralNumberIntegerLong */ .chroma .il { color:#a5d6ff } + /* LiteralNumberOct */ .chroma .mo { color:#a5d6ff } + /* Operator */ .chroma .o { color:#ff7b72;font-weight:bold } + /* OperatorWord */ .chroma .ow { color:#ff7b72;font-weight:bold } + /* OperatorReserved */ .chroma .or { color:#ff7b72;font-weight:bold } + /* Comment */ .chroma .c { color:#8b949e;font-style:italic } + /* CommentHashbang */ .chroma .ch { color:#8b949e;font-style:italic } + /* CommentMultiline */ .chroma .cm { color:#8b949e;font-style:italic } + /* CommentSingle */ .chroma .c1 { color:#8b949e;font-style:italic } + /* CommentSpecial */ .chroma .cs { color:#8b949e;font-weight:bold;font-style:italic } + /* CommentPreproc */ .chroma .cp { color:#8b949e;font-weight:bold;font-style:italic } + /* CommentPreprocFile */ .chroma .cpf { color:#8b949e;font-weight:bold;font-style:italic } + /* GenericDeleted */ .chroma .gd { color:#ffa198;background-color:#490202 } + /* GenericEmph */ .chroma .ge { font-style:italic } + /* GenericError */ .chroma .gr { color:#ffa198 } + /* GenericHeading */ .chroma .gh { color:#79c0ff;font-weight:bold } + /* GenericInserted */ .chroma .gi { color:#56d364;background-color:#0f5323 } + /* GenericOutput */ .chroma .go { color:#8b949e } + /* GenericPrompt */ .chroma .gp { color:#8b949e } + /* GenericStrong */ .chroma .gs { font-weight:bold } + /* GenericSubheading */ .chroma .gu { color:#79c0ff } + /* GenericTraceback */ .chroma .gt { color:#ff7b72 } + /* GenericUnderline */ .chroma .gl { text-decoration:underline } + /* TextWhitespace */ .chroma .w { color:#6e7681 } +} diff --git a/docs/assets/img/decolint.svg b/docs/assets/img/decolint.svg new file mode 100644 index 0000000..7ce7d35 --- /dev/null +++ b/docs/assets/img/decolint.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/img/decolint_logo.svg b/docs/assets/img/decolint_logo.svg new file mode 100644 index 0000000..7ded434 --- /dev/null +++ b/docs/assets/img/decolint_logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + decolint + \ No newline at end of file diff --git a/docs/assets/img/decolint_logo_dark.svg b/docs/assets/img/decolint_logo_dark.svg new file mode 100644 index 0000000..5e2038b --- /dev/null +++ b/docs/assets/img/decolint_logo_dark.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + decolint + \ No newline at end of file diff --git a/docs/content/rules/_index.md b/docs/content/rules/_index.md new file mode 100644 index 0000000..8c127d9 --- /dev/null +++ b/docs/content/rules/_index.md @@ -0,0 +1,10 @@ +--- +title: Rules +description: >- + Every rule decolint ships, what it checks, and why. +--- + +Each rule belongs to one category, which sets its severity unless a +[config file](../getting-started.md#2-turn-on-the-checks-you-want) overrides +it; only `correctness` is enabled out of the box. A rule that names a platform +runs only when that platform is selected with `-platform`. diff --git a/docs/data/categories.yaml b/docs/data/categories.yaml new file mode 100644 index 0000000..a8e193c --- /dev/null +++ b/docs/data/categories.yaml @@ -0,0 +1,14 @@ +# The categories a rule can belong to, in the order the rule reference presents them, which is +# decreasing order of how much a project is likely to want the category enabled. Each name must +# match a linter.Category name, which the rule pages carry in their front matter. +- name: correctness + blurb: >- + The configuration is invalid or does not behave as written. These run by + default, at `error`. +- name: security + blurb: Container runtime privileges and hardening. +- name: reproducibility + blurb: >- + Unpinned versions or digests that let the environment drift between builds. +- name: style + blurb: Discouraged or legacy configuration that still works. diff --git a/docs/hugo.toml b/docs/hugo.toml new file mode 100644 index 0000000..1dcdb52 --- /dev/null +++ b/docs/hugo.toml @@ -0,0 +1,74 @@ +baseURL = "https://bare-devcontainer.github.io/decolint/" +title = "decolint" +locale = "en-us" +enableRobotsTXT = true + +# A rule reference has no feed, tags or categories pages; the only taxonomy is the category each +# rule already carries in its front matter, which the rules section renders itself. +disableKinds = ["taxonomy", "term", "RSS"] + +# Findings link to a rule's page by an address derived from its ID (see rules.DocsURL), so the +# published paths have to stay /rules//. +[permalinks] + rules = "/rules/:slug/" + +[params] + description = "A linter for Dev Container configuration files." + repository = "https://github.com/bare-devcontainer/decolint" + +# Most content is generated by "go run ./cmd/docgen" (see the Makefile) into .generated/content — +# the rule pages from rules/*.go, and the landing/getting-started/reference pages from README.md — +# so it is mounted alongside the handful of pages (docs/content/rules/_index.md) that are still +# hand-written. Declaring any module mount opts out of Hugo's implicit defaults, so every directory +# the site needs is listed here. +[module] + [[module.mounts]] + source = "content" + target = "content" + [[module.mounts]] + source = ".generated/content" + target = "content" + [[module.mounts]] + source = "layouts" + target = "layouts" + [[module.mounts]] + source = "assets" + target = "assets" + [[module.mounts]] + source = "data" + target = "data" + +[markup.highlight] + # Emit token classes rather than inline styles, so the colors live in the site's own stylesheet + # and follow the reader's light or dark preference. + noClasses = false + +# Resolve links written as relative Markdown paths, e.g. [Rules](rules/no-app-port.md), to the +# published address of the page. Pages then read correctly both on the site and on GitHub. +[markup.goldmark.renderHooks.link] + useEmbedded = "fallback" + +[markup.goldmark.renderer] + # Rule pages are written by contributors and rendered as-is; nothing needs raw HTML. + unsafe = false + +# endLevel = 3 so Getting started's step-by-step h3 subsections ("### 1. Run it", ...) show in its +# sidebar TOC; Reference has only one h3 ("### Rule categories"), so this costs it nothing. +[markup.tableOfContents] + startLevel = 2 + endLevel = 3 + +[[menus.main]] + name = "Getting started" + pageRef = "/getting-started" + weight = 10 + +[[menus.main]] + name = "Rules" + pageRef = "/rules" + weight = 20 + +[[menus.main]] + name = "Reference" + pageRef = "/reference" + weight = 30 diff --git a/docs/layouts/baseof.html b/docs/layouts/baseof.html new file mode 100644 index 0000000..ee31773 --- /dev/null +++ b/docs/layouts/baseof.html @@ -0,0 +1,54 @@ + + + + + + {{ if .IsHome }}{{ site.Title }} — {{ site.Params.description }}{{ else }}{{ .Title }} — {{ site.Title }}{{ end }} + + {{ with .OutputFormats.Get "html" }}{{ end }} + {{ with resources.Get "img/decolint.svg" | minify | fingerprint }}{{ end }} + {{ range slice "css/main.css" "css/syntax.css" }} + {{ with resources.Get . | minify | fingerprint }} + + {{ end }} + {{ end }} + + + + + {{ $isRules := eq .Section "rules" }} + {{ $hasToc := .Params.toc }} +
+ {{ if $isRules }} + {{ partial "rule-nav.html" . }} + {{ else if $hasToc }} + {{ partial "page-toc.html" . }} + {{ end }} +
+ {{ block "main" . }}{{ end }} +
+
+ + + + diff --git a/docs/layouts/home.html b/docs/layouts/home.html new file mode 100644 index 0000000..4cfd224 --- /dev/null +++ b/docs/layouts/home.html @@ -0,0 +1,4 @@ +{{ define "main" }} +

{{ .Title }}

+ {{ .Content }} +{{ end }} diff --git a/docs/layouts/page.html b/docs/layouts/page.html new file mode 100644 index 0000000..a94a294 --- /dev/null +++ b/docs/layouts/page.html @@ -0,0 +1,19 @@ +{{ define "main" }} +
+

{{ .Title }}

+ {{/* A rule page states what the rule checks, and how it is scoped, from its front matter; the + body picks up at the reasoning. */}} + {{ if eq .Section "rules" }} +

{{ .RenderString .Description }}

+
+
Category
+
{{ .Params.category }}
+
Applies to
+
{{ delimit .Params.file_types ", " }}
+
Platforms
+
{{ with .Params.platforms }}{{ delimit . ", " }}{{ else }}all{{ end }}
+
+ {{ end }} + {{ .Content }} +
+{{ end }} diff --git a/docs/layouts/partials/page-toc.html b/docs/layouts/partials/page-toc.html new file mode 100644 index 0000000..583a56f --- /dev/null +++ b/docs/layouts/partials/page-toc.html @@ -0,0 +1,7 @@ +{{/* The sidebar shown alongside a long page (Getting started, Reference): a table of contents +built from the page's own ## headings, via Params.toc — see cmd/docgen and docs/hugo.toml's +[markup.tableOfContents]. */}} + diff --git a/docs/layouts/partials/rule-nav.html b/docs/layouts/partials/rule-nav.html new file mode 100644 index 0000000..3663329 --- /dev/null +++ b/docs/layouts/partials/rule-nav.html @@ -0,0 +1,17 @@ +{{/* The sidebar shown alongside every rule page: each category, then the rules in it. It is built +from the pages themselves, so it lists exactly what the site publishes. */}} +{{ $rules := where site.RegularPages "Section" "rules" }} + diff --git a/docs/layouts/rules/section.html b/docs/layouts/rules/section.html new file mode 100644 index 0000000..6a1e358 --- /dev/null +++ b/docs/layouts/rules/section.html @@ -0,0 +1,30 @@ +{{ define "main" }} +

{{ .Title }}

+ {{ .Content }} + + {{/* The table is built from the rule pages themselves, so it cannot fall behind them. That the + pages in turn match the rules decolint registers is what rules/doc_test.go checks. */}} + {{ $rules := where site.RegularPages "Section" "rules" }} + {{ range hugo.Data.categories }} + {{ $category := .name }} + {{ $blurb := .blurb }} + {{ with where $rules "Params.category" $category }} +

{{ $category }}

+

{{ $.RenderString $blurb }}

+ + + + + + {{ range sort . "Title" }} + + + + + + {{ end }} + +
RulePlatformChecks
{{ .Title }}{{ with .Params.platforms }}{{ delimit . ", " }}{{ else }}all{{ end }}{{ $.RenderString .Description }}
+ {{ end }} + {{ end }} +{{ end }} diff --git a/format/rules.go b/format/rules.go new file mode 100644 index 0000000..d79d7d1 --- /dev/null +++ b/format/rules.go @@ -0,0 +1,41 @@ +package format + +// RuleDoc is the full documentation of one built-in rule. It is the wire shape of +// "decolint -rules -format=json", and is what the documentation site is generated from (see +// cmd/docgen), so a reader of the JSON and a reader of the site see the same data. +type RuleDoc struct { + ID string `json:"id"` + Description string `json:"description"` + LongDescription string `json:"longDescription"` + References []string `json:"references"` + Category string `json:"category"` + Platforms []string `json:"platforms"` + FileTypes []string `json:"fileTypes"` + Example RuleExample `json:"example"` + // DocsURL is where the rule's page is published (see rules.DocsURL). + DocsURL string `json:"docsUrl"` + // Severity is the severity the rule is currently registered at: its category's default, unless + // overridden by the config file in effect. + Severity string `json:"severity"` +} + +// RuleExample is a [linter.Example] adapted for JSON. +type RuleExample struct { + Bad RuleSnippet `json:"bad"` + Good RuleSnippet `json:"good"` + Note string `json:"note,omitzero"` +} + +// RuleSnippet is a [linter.Snippet] adapted for JSON. +type RuleSnippet struct { + Files []RuleExampleFile `json:"files"` + DirName string `json:"dirName,omitzero"` +} + +// RuleExampleFile is a [linter.ExampleFile] adapted for JSON. +type RuleExampleFile struct { + Path string `json:"path"` + Content string `json:"content"` + // Mode is the file's POSIX permission bits (e.g. 420 for 0644), 0 meaning the default. + Mode uint32 `json:"mode,omitzero"` +} diff --git a/format/sarif.go b/format/sarif.go index 89d1e4b..5b6ac9e 100644 --- a/format/sarif.go +++ b/format/sarif.go @@ -20,6 +20,8 @@ type SARIFRule struct { ID string Description string Category string + // HelpURI is where the rule is documented in full. + HelpURI string } // SARIFFormat prints a SARIF 2.1.0 log, suitable for upload to GitHub Code Scanning. @@ -69,9 +71,11 @@ type sarifDriver struct { } type sarifRuleDescriptor struct { - ID string `json:"id"` - ShortDescription sarifMessage `json:"shortDescription,omitzero"` - Properties sarifProperties `json:"properties,omitzero"` + ID string `json:"id"` + ShortDescription sarifMessage `json:"shortDescription,omitzero"` + HelpURI string `json:"helpUri,omitzero"` + Help sarifMultiformatMessage `json:"help,omitzero"` + Properties sarifProperties `json:"properties,omitzero"` } type sarifProperties struct { @@ -82,6 +86,14 @@ type sarifMessage struct { Text string `json:"text"` } +// sarifMultiformatMessage is SARIF's multiformatMessageString: a plain-text rendering plus an +// optional Markdown one, which viewers prefer when they can render it. Text is required whenever +// Markdown is present. +type sarifMultiformatMessage struct { + Text string `json:"text"` + Markdown string `json:"markdown,omitzero"` +} + type sarifResult struct { RuleID string `json:"ruleId"` RuleIndex int `json:"ruleIndex"` @@ -140,6 +152,8 @@ func (f SARIFFormat) WriteReport(w io.Writer, report Report) error { desc := sarifRuleDescriptor{ID: id} if r, ok := catalog[id]; ok { desc.ShortDescription = sarifMessage{Text: r.Description} + desc.HelpURI = r.HelpURI + desc.Help = sarifHelpFor(r) desc.Properties = sarifProperties{Tags: []string{r.Category}} } descriptors = append(descriptors, desc) @@ -206,6 +220,20 @@ func (f SARIFFormat) WriteReport(w io.Writer, report Report) error { return nil } +// sarifHelpFor returns a link to the rule's documentation as SARIF help, the text a viewer shows +// when a reader asks why an alert was raised. GitHub Code Scanning renders the Markdown form on the +// alert but does not surface helpUri, so the link has to be in the message to be reachable. It is +// the zero value, and so omitted, for a rule with nowhere to link to. +func sarifHelpFor(r SARIFRule) sarifMultiformatMessage { + if r.HelpURI == "" { + return sarifMultiformatMessage{} + } + return sarifMultiformatMessage{ + Text: "Documentation: " + r.HelpURI, + Markdown: "[Rule documentation](" + r.HelpURI + ")", + } +} + // artifactURIFor returns the SARIF location of the file at path, which must be a URI rather than a // filesystem path. A relative path stays relative, for the consumer to resolve against the root of // the analyzed project; an absolute one, which decolint reports for a file outside the directory it diff --git a/format/sarif_test.go b/format/sarif_test.go index ebdb5d9..b32a38e 100644 --- a/format/sarif_test.go +++ b/format/sarif_test.go @@ -17,6 +17,7 @@ func testSARIFFormat() SARIFFormat { ID: "no-image-latest", Description: "images should be pinned to a specific version", Category: "reproducibility", + HelpURI: "https://example.invalid/rules/no-image-latest/", }, }, } @@ -32,7 +33,11 @@ func TestSARIFWriteReport(t *testing.T) { want := `{"$schema":"https://json.schemastore.org/sarif-2.1.0.json","version":"2.1.0","runs":[` + `{"tool":{"driver":{"name":"decolint","version":"1.2.3","informationUri":"https://github.com/bare-devcontainer/decolint","rules":[` + - `{"id":"no-image-latest","shortDescription":{"text":"images should be pinned to a specific version"},"properties":{"tags":["reproducibility"]}},` + + `{"id":"no-image-latest","shortDescription":{"text":"images should be pinned to a specific version"},` + + `"helpUri":"https://example.invalid/rules/no-image-latest/",` + + `"help":{"text":"Documentation: https://example.invalid/rules/no-image-latest/",` + + `"markdown":"[Rule documentation](https://example.invalid/rules/no-image-latest/)"},` + + `"properties":{"tags":["reproducibility"]}},` + `{"id":"some-error-rule"}]}},` + `"artifacts":[{"location":{"uri":".devcontainer/devcontainer.json"}},{"location":{"uri":"src/devcontainer-feature.json"}}],` + `"results":[` + @@ -138,7 +143,11 @@ func TestSARIFWriteReport_Empty(t *testing.T) { want := `{"$schema":"https://json.schemastore.org/sarif-2.1.0.json","version":"2.1.0","runs":[` + `{"tool":{"driver":{"name":"decolint","version":"1.2.3","informationUri":"https://github.com/bare-devcontainer/decolint","rules":[` + - `{"id":"no-image-latest","shortDescription":{"text":"images should be pinned to a specific version"},"properties":{"tags":["reproducibility"]}}]}},` + + `{"id":"no-image-latest","shortDescription":{"text":"images should be pinned to a specific version"},` + + `"helpUri":"https://example.invalid/rules/no-image-latest/",` + + `"help":{"text":"Documentation: https://example.invalid/rules/no-image-latest/",` + + `"markdown":"[Rule documentation](https://example.invalid/rules/no-image-latest/)"},` + + `"properties":{"tags":["reproducibility"]}}]}},` + `"artifacts":[],"results":[]}]}` + "\n" if sb.String() != want { @@ -146,6 +155,56 @@ func TestSARIFWriteReport_Empty(t *testing.T) { } } +// TestSARIFWriteReport_RuleHelp checks how a rule's documentation address becomes the descriptor's +// helpUri and help, including that a rule with nowhere to link to carries neither field. +func TestSARIFWriteReport_RuleHelp(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + rule SARIFRule + want []string + wantOmitted []string + }{ + { + name: "documented rule", + rule: SARIFRule{ID: "some-error-rule", HelpURI: "https://example.invalid/rules/some-error-rule/"}, + want: []string{ + `"helpUri":"https://example.invalid/rules/some-error-rule/"`, + `"help":{"text":"Documentation: https://example.invalid/rules/some-error-rule/",` + + `"markdown":"[Rule documentation](https://example.invalid/rules/some-error-rule/)"}`, + }, + }, + { + name: "no documentation address", + rule: SARIFRule{ID: "some-error-rule", Description: "short only"}, + want: []string{`"shortDescription":{"text":"short only"}`}, + wantOmitted: []string{`"helpUri"`, `"help"`}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + f := SARIFFormat{Version: "1.2.3", Rules: []SARIFRule{tt.rule}} + var sb strings.Builder + if err := f.WriteReport(&sb, Report{Issues: testIssues()[1:]}); err != nil { + t.Fatalf("WriteReport: %v", err) + } + for _, want := range tt.want { + if !strings.Contains(sb.String(), want) { + t.Errorf("WriteReport sarif = %q, want it to contain %q", sb.String(), want) + } + } + for _, omitted := range tt.wantOmitted { + if strings.Contains(sb.String(), omitted) { + t.Errorf("WriteReport sarif = %q, want it to omit %q", sb.String(), omitted) + } + } + }) + } +} + func TestSARIFWriteReport_WriteError(t *testing.T) { t.Parallel() diff --git a/go.mod b/go.mod index 128fafb..5aaa45a 100644 --- a/go.mod +++ b/go.mod @@ -16,58 +16,212 @@ require ( ) require ( + cel.dev/expr v0.25.1 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.20.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/monitoring v1.24.3 // indirect + cloud.google.com/go/storage v1.57.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 // indirect + github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect + github.com/JohannesKaufmann/dom v0.3.1 // indirect + github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2 // indirect github.com/agext/levenshtein v1.2.3 // indirect + github.com/alecthomas/chroma/v2 v2.27.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.42.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.24 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.23 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.12 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudfront v1.61.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect + github.com/aws/smithy-go v1.27.2 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/bep/clocks v0.5.0 // indirect + github.com/bep/debounce v1.2.0 // indirect + github.com/bep/gitmap v1.9.0 // indirect + github.com/bep/goat v0.5.0 // indirect + github.com/bep/godartsass/v2 v2.5.0 // indirect + github.com/bep/golibsass v1.2.0 // indirect + github.com/bep/golocales v0.2.0 // indirect + github.com/bep/goportabletext v0.2.0 // indirect + github.com/bep/helpers v0.12.0 // indirect + github.com/bep/imagemeta v0.17.2 // indirect + github.com/bep/lazycache v0.8.1 // indirect + github.com/bep/logg v0.4.0 // indirect + github.com/bep/mclib v1.20401.20400 // indirect + github.com/bep/overlayfs v0.11.0 // indirect + github.com/bep/simplecobra v0.7.0 // indirect + github.com/bep/textandbinarywriter v0.1.0 // indirect + github.com/bep/tmc v0.6.0 // indirect + github.com/bits-and-blooms/bitset v1.24.5 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clbanning/mxj/v2 v2.7.0 // indirect + github.com/clipperhouse/displaywidth v0.10.0 // indirect + github.com/clipperhouse/uax29/v2 v2.6.0 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/containerd/containerd/v2 v2.2.5 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v1.0.0-rc.4 // indirect github.com/containerd/ttrpc v1.2.8 // indirect github.com/containerd/typeurl/v2 v2.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/distribution/reference v0.6.0 // indirect + github.com/dlclark/regexp2/v2 v2.2.1 // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect + github.com/evanw/esbuild v0.28.1 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/frankban/quicktest v1.14.6 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/getkin/kin-openapi v0.140.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.23.1 // indirect + github.com/go-openapi/swag/jsonname v0.26.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/gobuffalo/flect v1.0.3 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/gohugoio/gift v0.2.0 // indirect + github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6 // indirect + github.com/gohugoio/go-radix v1.2.0 // indirect + github.com/gohugoio/hashstructure v0.6.0 // indirect + github.com/gohugoio/httpcache v0.8.0 // indirect + github.com/gohugoio/hugo v0.164.0 // indirect + github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0 // indirect + github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/google/s2a-go v0.1.9 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/google/wire v0.7.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.21.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/hairyhenderson/go-codeowners v0.7.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/in-toto/attestation v1.2.0 // indirect github.com/in-toto/in-toto-golang v0.11.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jdkato/prose v1.2.1 // indirect github.com/klauspost/compress v1.18.6 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/kyokomi/emoji/v2 v2.2.13 // indirect + github.com/magefile/mage v1.17.2 // indirect + github.com/makeworld-the-better-one/dither/v2 v2.4.0 // indirect + github.com/marekm4/color-extractor v1.2.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.2.0 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/patternmatcher v0.6.1 // indirect github.com/moby/sys/signal v0.7.1 // indirect + github.com/muesli/smartcrop v0.3.0 // indirect + github.com/niklasfasching/go-org v1.9.1 // indirect + github.com/oasdiff/yaml v0.1.0 // indirect + github.com/oasdiff/yaml3 v0.0.13 // indirect + github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect + github.com/olekukonko/errors v1.2.0 // indirect + github.com/olekukonko/ll v0.1.6 // indirect + github.com/olekukonko/tablewriter v1.1.4 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pelletier/go-toml/v2 v2.4.3 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect - github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect + github.com/rogpeppe/go-internal v1.15.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sirupsen/logrus v1.9.4 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/fsync v0.10.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/sudo-bmitch/oci-digest v0.1.2 // indirect + github.com/tdewolff/minify/v2 v2.24.13 // indirect + github.com/tdewolff/parse/v2 v2.8.12 // indirect + github.com/tetratelabs/wazero v1.12.0 // indirect github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect github.com/tonistiigi/fsutil v0.0.0-20260716115106-30cd4fc5d911 // indirect github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect + github.com/yuin/goldmark v1.8.2 // indirect + github.com/yuin/goldmark-emoji v1.0.6 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect + go.uber.org/automaxprocs v1.5.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect + gocloud.dev v0.45.0 // indirect golang.org/x/crypto v0.53.0 // indirect - golang.org/x/net v0.55.0 // indirect + golang.org/x/image v0.43.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/text v0.38.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.47.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/api v0.276.0 // indirect + google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/grpc v1.82.1 // indirect google.golang.org/protobuf v1.36.11 // indirect + howett.net/plist v1.0.1 // indirect + rsc.io/qr v0.2.0 // indirect + software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect ) + +tool github.com/gohugoio/hugo diff --git a/go.sum b/go.sum index 4e07f1a..eccaf80 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,207 @@ +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= +cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= +cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= +cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.57.2 h1:sVlym3cHGYhrp6XZKkKb+92I1V42ks2qKKpB0CF5Mb4= +cloud.google.com/go/storage v1.57.2/go.mod h1:n5ijg4yiRXXpCu0sJTD6k+eMf7GRrJmPyr9YxLXGHOk= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 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/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 h1:ZJJNFaQ86GVKQ9ehwqyAFE6pIfyicpuJ8IkVaPBc6/4= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3/go.mod h1:URuDvhmATVKqHBH9/0nOiNKk0+YcwfQ3WkK5PqHKxc8= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+WNGCVv7OmS5+lTc= +github.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 h1:4iB+IesclUXdP0ICgAabvq2FYLXrJWKx1fJQ+GxSo3Y= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU= +github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/JohannesKaufmann/dom v0.3.1 h1:J16l9JAHWgkFPR3VIPbQ1gvS0cWab6laK1q7PFL3qh0= +github.com/JohannesKaufmann/dom v0.3.1/go.mod h1:BZPkf8ZeYrBgABjwJn9iiKt8aiCtkxpHkevms+Yp2DE= +github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2 h1:XFJZFWESIWlUEHHjzBuv8RvrtCWnSGlimEX17ysSDb8= +github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2/go.mod h1:BHWO8lJzttJLqwuV8Rb1B3OG2OSzLbssZDI1FRg2eAA= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs= +github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8= +github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= +github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls= +github.com/aws/aws-sdk-go-v2/config v1.32.24 h1:aEDEj533yGdVvEHfkCY0D/1FbDrjnZr4pIulxRjqpHs= +github.com/aws/aws-sdk-go-v2/config v1.32.24/go.mod h1:yZtrGKJGlqfEW+/m2uTsJK+Jz7xF5R0eZfgcIG9m1ss= +github.com/aws/aws-sdk-go-v2/credentials v1.19.23 h1:Zhu3GOpRCkNjtE/gJpuPDsytSnaCCTQk8neAGsgzG5Y= +github.com/aws/aws-sdk-go-v2/credentials v1.19.23/go.mod h1:VsJF2ropPB37gDr7M2rLSpCE8IQWdpl62uae7qxZmqU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.12 h1:Zy6Tme1AA13kX8x3CnkHx5cqdGWGaj/anwOiWGnA0Xo= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.12/go.mod h1:ql4uXYKoTM9WUAUSmthY4AtPVrlTBZOvnBJTiCUdPxI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.61.1 h1:LSv6jOIn/yEsGLeL4TLggsLA+I+XbuZ8sKmUIEWKrzI= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.61.1/go.mod h1:XUduecWr236DyG8nZwJMewFbS4QcL8NZHxohdYDoPhM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 h1:V51LGlOq/1VsDsHUdoklAQi7rMmx4qQubvFYAlP2254= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22/go.mod h1:4Pzhyz8hJOm2bepgl+NjvRx8vlUFAIIvJnZ/MkcNPpU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 h1:hiME6pBzC7OTl9LMtlyTWBuEl1f4QBcUmFDKC7MLXtc= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 h1:JRseEu/vIDMaWis4bSw0QbXL+cvIGc1XnX076H5ZXLE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 h1:6Xt6Ztjkwdia/7EtEaG7ki/qZUYlCcd7tGUotQed1QE= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.5/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= +github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk= +github.com/aws/smithy-go v1.27.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= +github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU= +github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo= +github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/bep/gitmap v1.9.0 h1:2pyb1ex+cdwF6c4tsrhEgEKfyNfxE34d5K+s2sa9byc= +github.com/bep/gitmap v1.9.0/go.mod h1:Juq6e1qqCRvc1W7nzgadPGI9IGV13ZncEebg5atj4Vo= +github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA= +github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c= +github.com/bep/godartsass/v2 v2.5.0 h1:tKRvwVdyjCIr48qgtLa4gHEdtRkPF8H1OeEhJAEv7xg= +github.com/bep/godartsass/v2 v2.5.0/go.mod h1:rjsi1YSXAl/UbsGL85RLDEjRKdIKUlMQHr6ChUNYOFU= +github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI= +github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= +github.com/bep/golocales v0.2.0 h1:4H1H5UPw3ainpj5zykeEfiMRQngyaIC/t+I4Dvn+fvE= +github.com/bep/golocales v0.2.0/go.mod h1:Hl78nje8mNL3LzLeJvYN9NsIZgyFJGrGfvgO9r1+mwE= +github.com/bep/goportabletext v0.2.0 h1:CZ9f8jADBWqHwBymQiJJPCTSV/tHSA+PYzlUf86Yze0= +github.com/bep/goportabletext v0.2.0/go.mod h1:xDeA5+qcgKzJq6Q6XjAiBKtxLD3Yn7f6XP4joD3J3qU= +github.com/bep/helpers v0.12.0 h1:tD6V2DQW0B+FUynF2etR/106S/TO9akm+vA/Hk24GxY= +github.com/bep/helpers v0.12.0/go.mod h1:PfE7MGdA8sSQ19nyDh4tYbs5rAlStlJaDI21f/fnNps= +github.com/bep/imagemeta v0.17.2 h1:fDyXM1eAqCfBeqGLqS6UsN4OfuLM0cdu70KuLCehjOg= +github.com/bep/imagemeta v0.17.2/go.mod h1:+Hlp195TfZpzsqCxtDKTG6eWdyz2+F2V/oCYfr3CZKA= +github.com/bep/lazycache v0.8.1 h1:ko6ASLjkPxyV5DMWoNNZ8B2M0weyjqXX8IZkjBoBtvg= +github.com/bep/lazycache v0.8.1/go.mod h1:pbEiFsZoq7cLXvrTll0AHOPEurB1aGGxx4jKjOtlx9w= +github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ= +github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0= +github.com/bep/mclib v1.20401.20400 h1:silTOMNlNI7yHBb+HxEE0THIVFVWo/0I4SCH69FxtmU= +github.com/bep/mclib v1.20401.20400/go.mod h1:v5Hh3EIinPn7epigP28uf9JCkZlYzBS2vEOPe2wrHzM= +github.com/bep/overlayfs v0.11.0 h1:aymHDGC0CHpvn0XvTfgpK6skCp16oMi+tdUF32l6pPs= +github.com/bep/overlayfs v0.11.0/go.mod h1:L+ggdoKm+Y7Xb4a1osd+/LOPG4qsY62snqRqJH5Mspc= +github.com/bep/simplecobra v0.7.0 h1:kG8ZPwEc1o96hlIVGXcrrvwC8RornBqvMD3+pS0Z7y0= +github.com/bep/simplecobra v0.7.0/go.mod h1:PDXvBWH1ZMX05DRQ25ub/C6kKUuq+jROPgjbVz8wO1g= +github.com/bep/textandbinarywriter v0.1.0 h1:KXmXsRN2Uhwhm1G3e/snM8+5SPQBJrCEpIosdIBR3po= +github.com/bep/textandbinarywriter v0.1.0/go.mod h1:dAcHveajlWWU7PXhp6Dn4PIAYDg2H13Huif9xMS2w8w= +github.com/bep/tmc v0.6.0 h1:5zWy4L+3gS+Kk8czzLC4g7ETaC3wkX9ZtTRdAdL8V4s= +github.com/bep/tmc v0.6.0/go.mod h1:SNHxc3o2WSNMAYqJcAO0rxFY+pbhZzMwjIHe5xaAue0= +github.com/bits-and-blooms/bitset v1.24.5 h1:654xBVHc23gJMAgOTkPNoCVfiRxuIOAUnAZFtopqJ4w= +github.com/bits-and-blooms/bitset v1.24.5/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= +github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= +github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= +github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= +github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/compose-spec/compose-go/v2 v2.13.0 h1:2+2oS3v4SrtAOBdZRAZYBsBy47D571p5EXMSCppmTtE= @@ -20,43 +218,237 @@ github.com/containerd/ttrpc v1.2.8 h1:xbVu6D4qF2jihdh9rDVOKqUMiFBQk6YctTdo1zk087 github.com/containerd/ttrpc v1.2.8/go.mod h1:wyZW2K79t4Hfcxl+GUvkZqRBzJlqFFvgEeeWXa42tyE= github.com/containerd/typeurl/v2 v2.3.0 h1:HZHPhRWo5XMy3QGQoPrUzbW/2ckwjfweHmOwlkIrPAQ= github.com/containerd/typeurl/v2 v2.3.0/go.mod h1:Qk+PAdUYArVj41TnGi6rJ+48RF0PkcTc4i/taoBcK0w= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/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/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0= +github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/evanw/esbuild v0.28.1 h1:ds+yuRyUaZGx++GR56CrCeuXh8PVhVM4xq8v7PNELFc= +github.com/evanw/esbuild v0.28.1/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= +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.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/getkin/kin-openapi v0.140.0 h1:JFn675aXRFjyiZKa/BFWploGldQlI0gobp4J5k0EZ2g= +github.com/getkin/kin-openapi v0.140.0/go.mod h1:lISrB64F0CPcuDJ3LdtPTMJBY8VENjR9wJBdrcT6J3g= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= 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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= +github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= +github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= +github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= +github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= +github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 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/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= +github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gohugoio/gift v0.2.0 h1:vA31pP0rTVmBxBrhpY3WEt+4zM4g+1sDqYeemwsYeqc= +github.com/gohugoio/gift v0.2.0/go.mod h1:1Mrm5CjF33KpD749Dwj+UAjWZ3LC6cBXGuTMa5XwoP4= +github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6 h1:pxlAea9eRwuAnt/zKbGqlFO2ZszpIe24YpOVLf+N+4I= +github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6/go.mod h1:m5hu1im5Qc7LDycVLvee6MPobJiRLBYHklypFJR0/aE= +github.com/gohugoio/go-radix v1.2.0 h1:D5GTk8jIoeXirBSc2P4E4NdHKDrenk9k9N0ctU5Yrhg= +github.com/gohugoio/go-radix v1.2.0/go.mod h1:k6vDa0ebpbpgtzSj9lPGJcA4AZwJ9xUNObUy2vczPFM= +github.com/gohugoio/hashstructure v0.6.0 h1:7wMB/2CfXoThFYhdWRGv3u3rUM761Cq29CxUW+NltUg= +github.com/gohugoio/hashstructure v0.6.0/go.mod h1:lapVLk9XidheHG1IQ4ZSbyYrXcaILU1ZEP/+vno5rBQ= +github.com/gohugoio/httpcache v0.8.0 h1:hNdsmGSELztetYCsPVgjA960zSa4dfEqqF/SficorCU= +github.com/gohugoio/httpcache v0.8.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI= +github.com/gohugoio/hugo v0.164.0 h1:oDElnVJNvDMpEOeoxBjzW8JTFk7u+gJj2kG74BsfXlM= +github.com/gohugoio/hugo v0.164.0/go.mod h1:QPBCN6Gntmm6YrYJEnTasaZjnMLRkv8YoyCOw7mo9JE= +github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0 h1:I/n6v7VImJ3aISLnn73JAHXyjcQsMVvbguQPTk9Ehus= +github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0/go.mod h1:9LJNfKWFmhEJ7HW0in5znezMwH+FYMBIhNZ3VWtRcRs= +github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0 h1:p13Q0DBCrBRpJGtbtlgkYNCs4TnIlZJh8vHgnAiofrI= +github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0/go.mod h1:ob9PCHy/ocsQhTz68uxhyInaYCbbVNpOOrJkIoSeD+8= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/go-replayers/grpcreplay v1.3.0 h1:1Keyy0m1sIpqstQmgz307zhiJ1pV4uIlFds5weTmxbo= +github.com/google/go-replayers/grpcreplay v1.3.0/go.mod h1:v6NgKtkijC0d3e3RW8il6Sy5sqRVUwoQa4mHOGEy8DI= +github.com/google/go-replayers/httpreplay v1.2.0 h1:VM1wEyyjaoU53BwrOnaf9VhAyQQEEioJvFYxYcLRKzk= +github.com/google/go-replayers/httpreplay v1.2.0/go.mod h1:WahEFFZZ7a1P4VM1qEeHy+tME4bwyqPcwWbNlUI1Mcg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= +github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI= +github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo= +github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/in-toto/attestation v1.2.0 h1:aPRUZ3azbqD7yEBD5fP3TD8Dszf+YHo284SOcpahjQk= github.com/in-toto/attestation v1.2.0/go.mod h1:r79G45gOmzPismgObLSL+rZTFxUgZLOQJI6LofTZgXk= github.com/in-toto/in-toto-golang v0.11.0 h1:nfidMYBFx+E0lnmX5KUnN2Pdm8zdNKal1ayjJuzzRoA= github.com/in-toto/in-toto-golang v0.11.0/go.mod h1:u3PjTnwFKjp5a1YCcw8SJg0G+tMeKfVoWsWeFMDCMtw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU= +github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U= +github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= +github.com/magefile/mage v1.17.2 h1:fyXVu1eadI8Ap1HCCNgEhJ5McIWiYhLR8uol64ZZc40= +github.com/magefile/mage v1.17.2/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA= +github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE= +github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc= +github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0= +github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/buildkit v0.31.2 h1:UsaoSa0z45ycj+8DqWwmiXKnszDSNvv3wYPGLVtcEKE= github.com/moby/buildkit v0.31.2/go.mod h1:q1QO35x5YryiXLONScL2IybwMIziz6Rq3FBznz8fmGc= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -69,34 +461,112 @@ github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= +github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc= +github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= +github.com/neurosnap/sentences v1.0.6/go.mod h1:pg1IapvYpWCJJm/Etxeh0+gtMf1rI1STY9S7eUCPbDc= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0= +github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48= +github.com/oasdiff/yaml v0.1.0 h1:0bqZjfKc/8S9urj4JuwepX41WX9EoA6ifhU3SV06cXg= +github.com/oasdiff/yaml v0.1.0/go.mod h1:kOlRmMdL2X3vucLCEQO5u61SU22RysnfXvcttrZA1O0= +github.com/oasdiff/yaml3 v0.0.13 h1:06svmvOHOVBqF81+sY2EUScvUI/iS/vl2VIeUUxZQwg= +github.com/oasdiff/yaml3 v0.0.13/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/olareg/olareg v0.2.2 h1:H29V0+V2bIW4nRF3B1QAvooXts5eYS9dmQsD/chrBf0= github.com/olareg/olareg v0.2.2/go.mod h1:NxhWTjHaFU51m1+kwi2MZnakJlITH0s1tzdzCT3dXRQ= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= +github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= +github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA= +github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= +github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= +github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +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/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= -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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= +github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc= +github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs= github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/shogo82148/go-shuffle v0.0.0-20180218125048-27e6095f230d/go.mod h1:2htx6lmL0NGLHlO8ZCf+lQBGBHIbEujyywxJArf+2Yc= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +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= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/fsync v0.10.1 h1:JRnB7G72b+gIBaBcpn5ibJSd7ww1iEahXSX2B8G6dSE= +github.com/spf13/fsync v0.10.1/go.mod h1:y+B41vYq5i6Boa3Z+BVoPbDeOvxVkNU5OBXhoT8i4TQ= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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/sudo-bmitch/oci-digest v0.1.2 h1:are0qzWTsFZGZ3Uvdi9OSztJszSWaab6iqquMEEB7rw= github.com/sudo-bmitch/oci-digest v0.1.2/go.mod h1:SH6l5OIe0islKBZBedjiPOeET/0QwGL+/oYfQt51uQo= github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA= github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo= +github.com/tdewolff/minify/v2 v2.24.13 h1:xrcF7gKDnUszseEY9WX9mUlZII2v2Go/QAcAwRASw58= +github.com/tdewolff/minify/v2 v2.24.13/go.mod h1:emvwoYeIl8bfAKqRU5ww95LX9Gpggpqv/naal9a8Yq0= +github.com/tdewolff/parse/v2 v2.8.12 h1:5BBjfaCv482v3nltlS0u6wH1xJaxjR6ofDrWttNvROg= +github.com/tdewolff/parse/v2 v2.8.12/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= +github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= +github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= +github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= +github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= +github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0= github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4= github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY= github.com/tonistiigi/fsutil v0.0.0-20260716115106-30cd4fc5d911 h1:xJZz1fhsRSrGTzQ6wvh1gX6d5jQaYjbIuGXT2s0AWuM= @@ -105,8 +575,24 @@ github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10 github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= +github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= +github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 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/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 h1:MCcYL7J6Vt/X0kjqbMZkekCmwsurbQRbL69vkiye2lk= @@ -115,45 +601,371 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= +go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= 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/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +gocloud.dev v0.45.0 h1:WknIK8IbRdmynDvara3Q7G6wQhmEiOGwpgJufbM39sY= +gocloud.dev v0.45.0/go.mod h1:0kXKmkCLG6d31N7NyLZWzt7jDSQura9zD/mWgiB6THI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY= +golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.276.0 h1:nVArUtfLEihtW+b0DdcqRGK1xoEm2+ltAihyztq7MKY= +google.golang.org/api v0.276.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/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-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/neurosnap/sentences.v1 v1.0.6/go.mod h1:YlK+SN+fLQZj+kY3r8DkGDhDr91+S3JmTb5LSxFRQo0= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= +howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= oras.land/oras-go/v2 v2.6.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo= oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0= +software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/linter/rule.go b/linter/rule.go index a076d1f..c76cbf4 100644 --- a/linter/rule.go +++ b/linter/rule.go @@ -262,6 +262,13 @@ type Rule struct { ID string // Description is a short human-readable description of what the rule checks. Description string + // LongDescription explains why the rule exists: what goes wrong in the configuration it + // reports, and what to do instead. It is Markdown, and is shown wherever the rule is + // documented rather than merely named. + LongDescription string + // References are URLs to the specification, documentation, or implementation that justify the + // rule, most authoritative first. + References []string // Category is the [Category] this rule reports; every rule must declare exactly one. Category Category // FileTypes are the kinds of configuration files this rule applies to. @@ -274,8 +281,49 @@ type Rule struct { // matches any object member name or array index (e.g. "/mounts/*"); the empty string matches the // document root. Paths []string + // Example shows the rule firing and not firing on realistic configuration. Tests lint both: Bad + // must report the rule, Good must not. + Example Example // Check inspects one value matching Paths and returns any findings. It is called at most once per // rule for a given value, even if several patterns match it. Check must be safe for concurrent // use, since it may be called for multiple files. Check func(ctx *Context, node *Node) []Finding } + +// Example pairs configuration that trips a rule with configuration that doesn't, for the rule's +// documentation. +type Example struct { + // Bad is configuration the rule reports. + Bad Snippet + // Good is configuration the rule does not report, typically Bad with the problem fixed. + Good Snippet + // Note is Markdown prose shown after Good, for context Bad and Good alone don't convey (e.g. why + // Good is scoped the way it is). It is optional. + Note string +} + +// Snippet is the directory an example is linted in: one or more files, and optionally the +// directory's own name, for a rule that reads it (e.g. [Dir.Name]). +type Snippet struct { + // Files are the snippet's files. Exactly one must have the path a rule's first FileType is + // named at (e.g. "devcontainer.json" for [Devcontainer]); that is the file linted, and it is + // also the one shown first. The rest are context a rule reads from the directory, e.g. a + // devcontainer-template.json a devcontainer.json's ${templateOption:...} reference is checked + // against. + Files []ExampleFile + // DirName is the directory's own name, for a rule that reads [Dir.Name] (e.g. a Feature's or + // Template's id must match the directory containing it). It is empty when no rule needs it. + DirName string +} + +// ExampleFile is one file of a [Snippet]. +type ExampleFile struct { + // Path is the file's path, relative to the directory the example is linted in (e.g. + // "devcontainer.json" or ".devcontainer/devcontainer.json"). + Path string + // Content is the file's content. + Content string + // Mode is the file's permission bits. The zero value means a regular file at the default mode; + // set it for a rule that inspects permissions (e.g. whether install.sh is executable). + Mode fs.FileMode +} diff --git a/rules/conflicting_container_def.go b/rules/conflicting_container_def.go index b8740b6..bc39cba 100644 --- a/rules/conflicting_container_def.go +++ b/rules/conflicting_container_def.go @@ -11,10 +11,43 @@ import ( var ConflictingContainerDef = &linter.Rule{ ID: "conflicting-container-def", Description: `disallow a devcontainer.json that defines more than one of "image", "build", or "dockerComposeFile"`, - Category: linter.CategoryCorrectness, - FileTypes: []linter.FileType{linter.Devcontainer}, - Paths: []string{""}, - Check: checkConflictingContainerDef, + LongDescription: `The specification defines three mutually exclusive ways to create the container: from an image, from a +Dockerfile, or from a Docker Compose project. Which one wins when several are set is unspecified, so the +container that gets built depends on the tool rather than on the configuration. Keep the variant the +project actually uses and remove the others.`, + References: []string{ + `https://containers.dev/implementors/spec/#orchestration-options`, + `https://containers.dev/implementors/json_reference/#scenario-specific-properties`, + }, + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Devcontainer}, + Paths: []string{""}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "name": "my project", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "build": { + "dockerfile": "Dockerfile" + } +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "name": "my project", + "build": { + "dockerfile": "Dockerfile" + } +} +`}, + }, + }, + }, + Check: checkConflictingContainerDef, } func checkConflictingContainerDef(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/doc_test.go b/rules/doc_test.go new file mode 100644 index 0000000..3e6f45b --- /dev/null +++ b/rules/doc_test.go @@ -0,0 +1,149 @@ +package rules_test + +import ( + "net/url" + "runtime" + "slices" + "strings" + "testing" + "testing/fstest" + + "github.com/bare-devcontainer/decolint/linter" + "github.com/bare-devcontainer/decolint/rules" +) + +// Every built-in rule documents itself on its [linter.Rule]: a short Description, a LongDescription +// explaining why it exists, References that justify it, and an Example whose Bad configuration the +// rule must report and whose Good configuration it must not. These tests are what keeps a new rule +// from landing undocumented or shipping an example that doesn't actually demonstrate the rule, and +// what the documentation site (see cmd/docgen) and "decolint -rules -format=json" are generated from. + +func TestBuiltin_Descriptions(t *testing.T) { + t.Parallel() + + for _, reg := range rules.Builtin() { + if strings.TrimSpace(reg.Rule.Description) == "" { + t.Errorf("rule %s has no Description", reg.Rule.ID) + } + if strings.TrimSpace(reg.Rule.LongDescription) == "" { + t.Errorf("rule %s has no LongDescription", reg.Rule.ID) + } + if got := reg.Rule.LongDescription; got != strings.TrimSpace(got) { + t.Errorf("rule %s LongDescription has leading or trailing whitespace", reg.Rule.ID) + } + } +} + +func TestBuiltin_References(t *testing.T) { + t.Parallel() + + for _, reg := range rules.Builtin() { + if len(reg.Rule.References) == 0 { + t.Errorf("rule %s has no References", reg.Rule.ID) + } + for _, ref := range reg.Rule.References { + // A reference is rendered as a link wherever it is shown, so it has to be a URL a reader + // can follow on its own, not a bare path or a prose citation. + u, err := url.Parse(ref) + if err != nil { + t.Errorf("rule %s reference %q does not parse: %v", reg.Rule.ID, ref, err) + continue + } + if u.Scheme != "https" || u.Host == "" { + t.Errorf("rule %s reference %q is not an absolute https URL", reg.Rule.ID, ref) + } + } + if i := duplicateIndex(reg.Rule.References); i >= 0 { + t.Errorf("rule %s lists reference %q twice", reg.Rule.ID, reg.Rule.References[i]) + } + } +} + +// defaultFileName is the file name a rule's own configuration file is expected at, mirroring +// discovery's naming for each [linter.FileType]. +var defaultFileName = map[linter.FileType]string{ + linter.Devcontainer: "devcontainer.json", + linter.Feature: "devcontainer-feature.json", + linter.Template: "devcontainer-template.json", +} + +// TestBuiltin_Examples lints each rule's Example.Bad and Example.Good, exercising the same +// path-matching and traversal logic the linter uses in production: Bad must report the rule and Good +// must not, so an example cannot drift from the rule it documents. +func TestBuiltin_Examples(t *testing.T) { + t.Parallel() + + for _, reg := range rules.Builtin() { + t.Run(reg.Rule.ID, func(t *testing.T) { + t.Parallel() + + // The exec-bit rule reports nothing on Windows by design (see + // checkFeatureInstallScriptNotExecutable); its Bad example would report zero findings + // there too, which would otherwise look like example drift rather than the documented + // platform limitation. + if reg.Rule.ID == "feature-install-script-not-executable" && runtime.GOOS == "windows" { + t.Skip("this rule does not run on Windows") + } + + if len(reg.Rule.Example.Bad.Files) == 0 || len(reg.Rule.Example.Good.Files) == 0 { + t.Fatalf("rule %s has an empty Example", reg.Rule.ID) + } + + if issues := lintExample(t, reg.Rule, reg.Rule.Example.Bad); len(issues) == 0 { + t.Errorf("Example.Bad reports nothing; it must trip %s", reg.Rule.ID) + } + if issues := lintExample(t, reg.Rule, reg.Rule.Example.Good); len(issues) > 0 { + t.Errorf("Example.Good reports %d issue(s), want none: %v", len(issues), issues) + } + }) + } +} + +// lintExample lints snip with r as the only active rule, registered at [linter.SeverityError], and +// returns what it reports. Every file in snip is visible to a rule that reads sibling files; the one +// named after r's own file type is the one linted. +func lintExample(t *testing.T, r *linter.Rule, snip linter.Snippet) []linter.Issue { + t.Helper() + + fileName, ok := defaultFileName[r.FileTypes[0]] + if !ok { + t.Fatalf("rule %s FileTypes[0] %q has no default file name", r.ID, r.FileTypes[0]) + } + + fsys := fstest.MapFS{} + var source string + var found bool + for _, f := range snip.Files { + mode := f.Mode + if mode == 0 { + mode = 0o644 + } + fsys[f.Path] = &fstest.MapFile{Data: []byte(f.Content), Mode: mode} + if f.Path == fileName { + source, found = f.Content, true + } + } + if !found { + t.Fatalf("example has no %s to lint, got %d file(s)", fileName, len(snip.Files)) + } + + doc, err := linter.ParseDocument([]byte(source)) + if err != nil { + t.Fatalf("parse %s example: %v", fileName, err) + } + + l := linter.New() + l.RegisterRule(r, linter.SeverityError) + return l.LintDocument(fileName, r.FileTypes[0], doc, linter.Dir{FS: fsys, Name: snip.DirName}) +} + +// duplicateIndex returns the index of the first element of refs that appears earlier in it, or -1 +// if every element is unique. +func duplicateIndex(refs []string) int { + for i, ref := range refs { + if slices.Contains(refs[:i], ref) { + return i + } + } + return -1 +} diff --git a/rules/docs.go b/rules/docs.go new file mode 100644 index 0000000..4fe2a35 --- /dev/null +++ b/rules/docs.go @@ -0,0 +1,12 @@ +package rules + +// docsBaseURL is where the rule reference is published. The pages themselves live in the +// repository's docs directory, one Markdown file per rule ID. +const docsBaseURL = "https://bare-devcontainer.github.io/decolint/rules/" + +// DocsURL returns the address of the page documenting the rule with the given ID: what it checks, +// why, and configuration it accepts and rejects. The address is derived from the ID, so it is +// returned for any id, including one no built-in rule has. +func DocsURL(id string) string { + return docsBaseURL + id + "/" +} diff --git a/rules/feature_install_script_not_executable.go b/rules/feature_install_script_not_executable.go index 9ba8c4b..4e1d4ff 100644 --- a/rules/feature_install_script_not_executable.go +++ b/rules/feature_install_script_not_executable.go @@ -14,12 +14,47 @@ import ( var FeatureInstallScriptNotExecutable = &linter.Rule{ ID: "feature-install-script-not-executable", Description: "disallow a Feature's `install.sh` that lacks executable permission bits", - Category: linter.CategoryCorrectness, - FileTypes: []linter.FileType{linter.Feature}, - Paths: []string{""}, - Check: checkFeatureInstallScriptNotExecutable, + LongDescription: `The specification has the installing tool invoke "install.sh" directly rather than through a shell, so +that the script's own shebang selects the interpreter. That requires the execute bit: without it the +Feature fails to install when a container is built. Run "chmod +x install.sh" and commit the mode change.`, + References: []string{ + `https://containers.dev/implementors/features/#invoking-installsh`, + }, + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Feature}, + Paths: []string{""}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: "devcontainer-feature.json", Content: featureInstallScriptExampleFeature}, + {Path: installScriptName, Content: featureInstallScriptExampleScript, Mode: 0o644}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: "devcontainer-feature.json", Content: featureInstallScriptExampleFeature}, + {Path: installScriptName, Content: featureInstallScriptExampleScript, Mode: 0o755}, + }, + }, + Note: "Git records the executable bit, so committing the mode change is what makes\n" + + "the fix stick. On Windows, where the filesystem has no executable bit, set it\n" + + "in the index directly: `git update-index --chmod=+x install.sh`.", + }, + Check: checkFeatureInstallScriptNotExecutable, } +const featureInstallScriptExampleFeature = `{ + "id": "node", + "version": "1.0.0", + "name": "Node.js" +} +` + +const featureInstallScriptExampleScript = `#!/usr/bin/env bash +set -e +apt-get update && apt-get install -y nodejs +` + func checkFeatureInstallScriptNotExecutable(ctx *linter.Context, node *linter.Node) []linter.Finding { // Windows working trees carry no executable bits (git does not set them there), so the check // would report every install.sh as non-executable. Skip it rather than emit false positives. diff --git a/rules/id_dir_mismatch.go b/rules/id_dir_mismatch.go index 9256d12..4593c78 100644 --- a/rules/id_dir_mismatch.go +++ b/rules/id_dir_mismatch.go @@ -12,10 +12,44 @@ import ( var IDDirMismatch = &linter.Rule{ ID: "id-dir-mismatch", Description: `disallow a Feature's or Template's "id" that does not match the name of its containing directory`, - Category: linter.CategoryCorrectness, - FileTypes: []linter.FileType{linter.Feature, linter.Template}, - Paths: []string{"/id"}, - Check: checkIDDirMismatch, + LongDescription: `Both specifications require the "id" to match the name of the directory holding the metadata file, since +that directory name is what packaging and distribution address the artifact by. When the two disagree the +published reference does not resolve to what the directory contains; rename the directory or the "id" so +they agree.`, + References: []string{ + `https://containers.dev/implementors/features/#devcontainer-featurejson-properties`, + `https://containers.dev/implementors/templates/#devcontainer-templatejson-properties`, + }, + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Feature, linter.Template}, + Paths: []string{"/id"}, + Example: linter.Example{ + Bad: linter.Snippet{ + DirName: "node", + Files: []linter.ExampleFile{ + {Path: `devcontainer-feature.json`, Content: `// src/node/devcontainer-feature.json +{ + "id": "nodejs", + "version": "1.0.0", + "name": "Node.js" +} +`}, + }, + }, + Good: linter.Snippet{ + DirName: "node", + Files: []linter.ExampleFile{ + {Path: `devcontainer-feature.json`, Content: `// src/node/devcontainer-feature.json +{ + "id": "node", + "version": "1.0.0", + "name": "Node.js" +} +`}, + }, + }, + }, + Check: checkIDDirMismatch, } func checkIDDirMismatch(ctx *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/invalid_semver.go b/rules/invalid_semver.go index 424cd6e..552753c 100644 --- a/rules/invalid_semver.go +++ b/rules/invalid_semver.go @@ -13,10 +13,39 @@ import ( var InvalidSemver = &linter.Rule{ ID: "invalid-semver", Description: `disallow a Feature's or Template's "version" that is not a valid semantic version`, - Category: linter.CategoryCorrectness, - FileTypes: []linter.FileType{linter.Feature, linter.Template}, - Paths: []string{"/version"}, - Check: checkInvalidSemver, + LongDescription: `Publishing a Feature or Template pushes it under tags derived from the "version" components: the full +version, "major.minor", and "major", so consumers can pin as loosely or as tightly as they want. A value +that is not valid semver has no such components, leaving nothing to derive those tags from.`, + References: []string{ + `https://containers.dev/implementors/features-distribution/#versioning`, + `https://semver.org/`, + }, + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Feature, linter.Template}, + Paths: []string{"/version"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer-feature.json`, Content: `{ + "id": "node", + "version": "1.0", + "name": "Node.js" +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer-feature.json`, Content: `{ + "id": "node", + "version": "1.0.0", + "name": "Node.js" +} +`}, + }, + }, + }, + Check: checkInvalidSemver, } // semverPattern is the official semantic version regular expression published at diff --git a/rules/missing_build_dockerfile.go b/rules/missing_build_dockerfile.go index 452879e..055c060 100644 --- a/rules/missing_build_dockerfile.go +++ b/rules/missing_build_dockerfile.go @@ -10,10 +10,42 @@ import ( var MissingBuildDockerfile = &linter.Rule{ ID: "missing-build-dockerfile", Description: `disallow a devcontainer.json "build" object that is missing "dockerfile"`, - Category: linter.CategoryCorrectness, - FileTypes: []linter.FileType{linter.Devcontainer}, - Paths: []string{"/build"}, - Check: checkMissingBuildDockerfile, + LongDescription: `"build.dockerfile" is the only required member of "build": it locates, relative to the devcontainer.json, +the Dockerfile the image is built from. The other members ("context", "args", "target", ...) only shape a +build that "dockerfile" defines, so without it there is nothing to build.`, + References: []string{ + `https://containers.dev/implementors/json_reference/#image-or-dockerfile-specific-properties`, + `https://containers.dev/implementors/spec/#dockerfile-based`, + }, + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Devcontainer}, + Paths: []string{"/build"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "name": "my project", + "build": { + "context": ".." + } +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "name": "my project", + "build": { + "dockerfile": "Dockerfile", + "context": ".." + } +} +`}, + }, + }, + }, + Check: checkMissingBuildDockerfile, } func checkMissingBuildDockerfile(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/missing_compose_service.go b/rules/missing_compose_service.go index 6bad04f..a654a15 100644 --- a/rules/missing_compose_service.go +++ b/rules/missing_compose_service.go @@ -10,10 +10,40 @@ import ( var MissingComposeService = &linter.Rule{ ID: "missing-compose-service", Description: `disallow a devcontainer.json that sets "dockerComposeFile" without "service"`, - Category: linter.CategoryCorrectness, - FileTypes: []linter.FileType{linter.Devcontainer}, - Paths: []string{""}, - Check: checkMissingComposeService, + LongDescription: `A Compose project usually defines several services, so naming the Compose file does not say which +container the tooling should attach to. The specification requires "service" to name that main container: +it is the one lifecycle scripts run in and the one editors connect to.`, + References: []string{ + `https://containers.dev/implementors/spec/#docker-compose-based`, + `https://containers.dev/implementors/json_reference/#docker-compose-specific-properties`, + }, + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Devcontainer}, + Paths: []string{""}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "name": "my project", + "dockerComposeFile": "docker-compose.yml", + "workspaceFolder": "/workspace" +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "name": "my project", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspace" +} +`}, + }, + }, + }, + Check: checkMissingComposeService, } func checkMissingComposeService(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/missing_container_def.go b/rules/missing_container_def.go index 2f432e8..8c26e27 100644 --- a/rules/missing_container_def.go +++ b/rules/missing_container_def.go @@ -10,10 +10,38 @@ import ( var MissingContainerDef = &linter.Rule{ ID: "missing-container-def", Description: `disallow a devcontainer.json that defines none of "image", "build", or "dockerComposeFile"`, - Category: linter.CategoryCorrectness, - FileTypes: []linter.FileType{linter.Devcontainer}, - Paths: []string{""}, - Check: checkMissingContainerDef, + LongDescription: `Every dev container is created from exactly one of "image", "build", or "dockerComposeFile", and each of +the three is required in its own scenario. A configuration that sets none of them describes no container +at all, so no tool can create one from it.`, + References: []string{ + `https://containers.dev/implementors/spec/#orchestration-options`, + `https://containers.dev/implementors/json_reference/#scenario-specific-properties`, + }, + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Devcontainer}, + Paths: []string{""}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "name": "my project", + "forwardPorts": [3000] +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "name": "my project", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "forwardPorts": [3000] +} +`}, + }, + }, + }, + Check: checkMissingContainerDef, } func checkMissingContainerDef(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/missing_feature_install_script.go b/rules/missing_feature_install_script.go index 0b16cd3..36d708a 100644 --- a/rules/missing_feature_install_script.go +++ b/rules/missing_feature_install_script.go @@ -16,10 +16,32 @@ const installScriptName = "install.sh" var MissingFeatureInstallScript = &linter.Rule{ ID: "missing-feature-install-script", Description: "disallow a Feature directory without the required `install.sh` install script", - Category: linter.CategoryCorrectness, - FileTypes: []linter.FileType{linter.Feature}, - Paths: []string{""}, - Check: checkMissingFeatureInstallScript, + LongDescription: `A Feature is distributed as its metadata file plus the "install.sh" the tooling runs inside the container, +which is where the Feature does all of its work. A directory without one publishes a Feature that +installs nothing, and the omission only surfaces when someone builds a container with it.`, + References: []string{ + `https://containers.dev/implementors/features/#folder-structure`, + `https://containers.dev/implementors/features/#invoking-installsh`, + }, + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Feature}, + Paths: []string{""}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: "devcontainer-feature.json", Content: featureInstallScriptExampleFeature}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: "devcontainer-feature.json", Content: featureInstallScriptExampleFeature}, + {Path: installScriptName, Content: featureInstallScriptExampleScript, Mode: 0o755}, + }, + }, + Note: "The name is fixed: the tooling runs `install.sh` and nothing else, so an\n" + + "install script under any other name is never executed.", + }, + Check: checkMissingFeatureInstallScript, } func checkMissingFeatureInstallScript(ctx *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/missing_required_props.go b/rules/missing_required_props.go index eeebe46..bab6e96 100644 --- a/rules/missing_required_props.go +++ b/rules/missing_required_props.go @@ -12,10 +12,38 @@ import ( var MissingRequiredProps = &linter.Rule{ ID: "missing-required-props", Description: `disallow a Feature's or Template's metadata that is missing a required property ("id", "version", or "name")`, - Category: linter.CategoryCorrectness, - FileTypes: []linter.FileType{linter.Feature, linter.Template}, - Paths: []string{""}, - Check: checkMissingRequiredProps, + LongDescription: `"id", "version", and "name" are the only properties either specification requires: the "id" addresses the +artifact, the "version" is what consumers pin to, and the "name" is what a user recognizes it by in a +list. Metadata missing any of them cannot be published as a usable Feature or Template.`, + References: []string{ + `https://containers.dev/implementors/features/#devcontainer-featurejson-properties`, + `https://containers.dev/implementors/templates/#devcontainer-templatejson-properties`, + }, + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Feature, linter.Template}, + Paths: []string{""}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer-feature.json`, Content: `{ + "id": "node", + "version": "1.0.0" +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer-feature.json`, Content: `{ + "id": "node", + "version": "1.0.0", + "name": "Node.js" +} +`}, + }, + }, + }, + Check: checkMissingRequiredProps, } func checkMissingRequiredProps(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/missing_workspace_mount_folder.go b/rules/missing_workspace_mount_folder.go index c59d90e..cd826a2 100644 --- a/rules/missing_workspace_mount_folder.go +++ b/rules/missing_workspace_mount_folder.go @@ -13,10 +13,39 @@ import ( var MissingWorkspaceMountFolder = &linter.Rule{ ID: "missing-workspace-mount-folder", Description: `disallow a devcontainer.json using "image" or "build" that sets only one of "workspaceMount" or "workspaceFolder"`, - Category: linter.CategoryCorrectness, - FileTypes: []linter.FileType{linter.Devcontainer}, - Paths: []string{""}, - Check: checkMissingWorkspaceMountFolder, + LongDescription: `The two properties describe opposite ends of the same override: "workspaceMount" says where the source +code is mounted, "workspaceFolder" says which path inside the container the tooling opens. The reference +documents each as requiring the other, because setting one alone either mounts the source somewhere +nothing opens, or opens a path nothing is mounted at.`, + References: []string{ + `https://containers.dev/implementors/json_reference/#image-or-dockerfile-specific-properties`, + `https://containers.dev/implementors/spec/#workspacefolder-and-workspacemount`, + }, + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Devcontainer}, + Paths: []string{""}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "workspaceMount": "source=${localWorkspaceFolder},target=/srv/app,type=bind" +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "workspaceMount": "source=${localWorkspaceFolder},target=/srv/app,type=bind", + "workspaceFolder": "/srv/app" +} +`}, + }, + }, + }, + Check: checkMissingWorkspaceMountFolder, } func checkMissingWorkspaceMountFolder(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/no_app_port.go b/rules/no_app_port.go index dc9bbd5..a71946b 100644 --- a/rules/no_app_port.go +++ b/rules/no_app_port.go @@ -8,10 +8,38 @@ import "github.com/bare-devcontainer/decolint/linter" var NoAppPort = &linter.Rule{ ID: "no-app-port", Description: `disallow the legacy "appPort" property in favor of "forwardPorts"`, - Category: linter.CategoryStyle, - FileTypes: []linter.FileType{linter.Devcontainer}, - Paths: []string{"/appPort"}, - Check: checkNoAppPort, + LongDescription: `"appPort" publishes the port the way Docker does: it is fixed when the container is created, and the +application has to listen on all interfaces rather than just "localhost" to be reachable. A forwarded +port instead looks like "localhost" to the application and can be changed without recreating the +container, which is why the reference recommends "forwardPorts" in most cases.`, + References: []string{ + `https://containers.dev/implementors/json_reference/#image-or-dockerfile-specific-properties`, + `https://containers.dev/implementors/json_reference/#publishing-vs-forwarding-ports`, + }, + Category: linter.CategoryStyle, + FileTypes: []linter.FileType{linter.Devcontainer}, + Paths: []string{"/appPort"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "appPort": [3000] +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "forwardPorts": [3000] +} +`}, + }, + }, + }, + Check: checkNoAppPort, } func checkNoAppPort(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/no_bind_mount.go b/rules/no_bind_mount.go index f087d4d..ecfab93 100644 --- a/rules/no_bind_mount.go +++ b/rules/no_bind_mount.go @@ -10,11 +10,51 @@ import ( var NoBindMount = &linter.Rule{ ID: "no-bind-mount", Description: `disallow "bind" type entries in "mounts", which GitHub Codespaces silently ignores except for the Docker socket`, - Category: linter.CategoryCorrectness, - FileTypes: []linter.FileType{linter.Devcontainer}, - Platforms: []linter.Platform{linter.PlatformCodespaces}, - Paths: []string{"/mounts/*"}, - Check: checkNoBindMount, + LongDescription: `A codespace runs on a machine in the cloud, where the host path a bind mount points at does not exist, so +Codespaces documents that it ignores "bind" mounts apart from the Docker socket. The mount is dropped +without an error and the container starts missing the data it expects. Volume mounts are honored, so use +"type=volume" for anything that only has to persist across rebuilds.`, + References: []string{ + `https://github.com/devcontainers/spec/blob/main/docs/specs/supporting-tools.md#github-codespaces`, + `https://containers.dev/implementors/spec/#mounts`, + }, + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Devcontainer}, + Platforms: []linter.Platform{linter.PlatformCodespaces}, + Paths: []string{"/mounts/*"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "mounts": [ + { + "source": "${localWorkspaceFolder}/.cache", + "target": "/home/vscode/.cache", + "type": "bind" + } + ] +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "mounts": [ + { + "source": "devcontainer-cache", + "target": "/home/vscode/.cache", + "type": "volume" + } + ] +} +`}, + }, + }, + }, + Check: checkNoBindMount, } func checkNoBindMount(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/no_cap_add_all.go b/rules/no_cap_add_all.go index c3505da..68ca18a 100644 --- a/rules/no_cap_add_all.go +++ b/rules/no_cap_add_all.go @@ -12,10 +12,38 @@ import ( var NoCapAddAll = &linter.Rule{ ID: "no-cap-add-all", Description: `disallow granting all Linux capabilities via an "ALL" entry in the "capAdd" property, or a "--cap-add=ALL" entry in a devcontainer.json's "runArgs"`, - Category: linter.CategorySecurity, - FileTypes: []linter.FileType{linter.Devcontainer, linter.Feature}, - Paths: []string{"/capAdd/*", "/runArgs"}, - Check: checkNoCapAddAll, + LongDescription: `Linux capabilities split root's powers into units a container can be granted individually, and the runtime +withholds the dangerous ones by default. "ALL" hands them all over, including capabilities such as +"SYS_ADMIN" and "SYS_MODULE" that let a process reconfigure the host kernel and escape the container. +"capAdd" exists to name the one or two a workload actually needs, e.g. "SYS_PTRACE" for a debugger.`, + References: []string{ + `https://containers.dev/implementors/json_reference/#general-devcontainerjson-properties`, + `https://docs.docker.com/engine/security/#linux-kernel-capabilities`, + }, + Category: linter.CategorySecurity, + FileTypes: []linter.FileType{linter.Devcontainer, linter.Feature}, + Paths: []string{"/capAdd/*", "/runArgs"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "capAdd": ["ALL"] +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "capAdd": ["SYS_PTRACE"] +} +`}, + }, + }, + }, + Check: checkNoCapAddAll, } func checkNoCapAddAll(ctx *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/no_docker_socket_mount.go b/rules/no_docker_socket_mount.go index df903eb..0eaef14 100644 --- a/rules/no_docker_socket_mount.go +++ b/rules/no_docker_socket_mount.go @@ -14,10 +14,47 @@ import ( var NoDockerSocketMount = &linter.Rule{ ID: "no-docker-socket-mount", Description: `disallow bind-mounting the host's Docker socket via a devcontainer.json's "mounts" or "runArgs", which grants the container root-equivalent control over the host`, - Category: linter.CategorySecurity, - FileTypes: []linter.FileType{linter.Devcontainer}, - Paths: []string{"/mounts/*", "/runArgs/*"}, - Check: checkNoDockerSocketMount, + LongDescription: `The Docker socket is the daemon's full API, and the daemon runs as root on the host. Anything that can +reach the socket can start a container that mounts the host's filesystem, so mounting it into the dev +container hands root-equivalent control of the host to every process inside — including code the +project's own build fetches. When the container genuinely needs Docker, a Docker-in-Docker Feature or a +rootless daemon keeps that access inside the container.`, + References: []string{ + `https://containers.dev/implementors/json_reference/#general-devcontainerjson-properties`, + `https://docs.docker.com/engine/security/#docker-daemon-attack-surface`, + }, + Category: linter.CategorySecurity, + FileTypes: []linter.FileType{linter.Devcontainer}, + Paths: []string{"/mounts/*", "/runArgs/*"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "mounts": [ + { + "source": "/var/run/docker.sock", + "target": "/var/run/docker.sock", + "type": "bind" + } + ] +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2.13.0": {} + } +} +`}, + }, + }, + }, + Check: checkNoDockerSocketMount, } func checkNoDockerSocketMount(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/no_host_port_format.go b/rules/no_host_port_format.go index 31fd1d6..9af3a6f 100644 --- a/rules/no_host_port_format.go +++ b/rules/no_host_port_format.go @@ -15,11 +15,39 @@ import ( var NoHostPortFormat = &linter.Rule{ ID: "no-host-port-format", Description: `disallow "host:port" entries in "forwardPorts" and "portsAttributes", which GitHub Codespaces does not support`, - Category: linter.CategoryCorrectness, - FileTypes: []linter.FileType{linter.Devcontainer}, - Platforms: []linter.Platform{linter.PlatformCodespaces}, - Paths: []string{"/forwardPorts/*", "/portsAttributes/*"}, - Check: checkNoHostPortFormat, + LongDescription: `The "host:port" form forwards a port from another container in a Docker Compose project (e.g. "db:5432") +rather than from the primary one. Codespaces documents that it does not support that variation of either +property, so the entry is ignored there and the port is not forwarded. A bare port number, which refers +to the primary container, works everywhere.`, + References: []string{ + `https://github.com/devcontainers/spec/blob/main/docs/specs/supporting-tools.md#github-codespaces`, + `https://containers.dev/implementors/json_reference/#general-devcontainerjson-properties`, + }, + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Devcontainer}, + Platforms: []linter.Platform{linter.PlatformCodespaces}, + Paths: []string{"/forwardPorts/*", "/portsAttributes/*"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "forwardPorts": ["db:5432"] +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "forwardPorts": [5432] +} +`}, + }, + }, + }, + Check: checkNoHostPortFormat, } func checkNoHostPortFormat(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/no_image_latest.go b/rules/no_image_latest.go index 5df6680..3d6a9ec 100644 --- a/rules/no_image_latest.go +++ b/rules/no_image_latest.go @@ -13,10 +13,35 @@ import ( var NoImageLatest = &linter.Rule{ ID: "no-image-latest", Description: `disallow container images without an explicit tag or with the "latest" tag`, - Category: linter.CategoryReproducibility, - FileTypes: []linter.FileType{linter.Devcontainer}, - Paths: []string{"/image"}, - Check: checkNoImageLatest, + LongDescription: `A reference with no tag resolves to "latest", and "latest" is just the tag a publisher moves as they +release. Either way the configuration says "whatever is current", so the same devcontainer.json builds a +different environment next month, and a build that broke cannot be reproduced from the file alone. Name +the version the project was tested against.`, + References: []string{ + `https://containers.dev/implementors/json_reference/#image-or-dockerfile-specific-properties`, + }, + Category: linter.CategoryReproducibility, + FileTypes: []linter.FileType{linter.Devcontainer}, + Paths: []string{"/image"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:latest" +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04" +} +`}, + }, + }, + }, + Check: checkNoImageLatest, } func checkNoImageLatest(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/no_privileged_container.go b/rules/no_privileged_container.go index fb9df83..a27c244 100644 --- a/rules/no_privileged_container.go +++ b/rules/no_privileged_container.go @@ -12,10 +12,43 @@ import ( var NoPrivilegedContainer = &linter.Rule{ ID: "no-privileged-container", Description: `disallow running the container in privileged mode via the "privileged" property or a "--privileged" entry in "runArgs"`, - Category: linter.CategorySecurity, - FileTypes: []linter.FileType{linter.Devcontainer, linter.Feature}, - Paths: []string{"/privileged", "/runArgs/*"}, - Check: checkNoPrivilegedContainer, + LongDescription: `A privileged container gets every Linux capability, unconfined seccomp and LSM profiles, and access to all +host devices. That removes essentially every boundary between the container and the host, so any code +running in it — including a compromised dependency pulled in by the project's own build — can take over +the machine. Docker-in-Docker is the usual reason it is set; a Feature that provides it, or the specific +capabilities and devices the workload needs, is a far narrower grant.`, + References: []string{ + `https://containers.dev/implementors/json_reference/#general-devcontainerjson-properties`, + `https://docs.docker.com/engine/security/#docker-daemon-attack-surface`, + }, + Category: linter.CategorySecurity, + FileTypes: []linter.FileType{linter.Devcontainer, linter.Feature}, + Paths: []string{"/privileged", "/runArgs/*"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "privileged": true +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "capAdd": ["SYS_PTRACE"] +} +`}, + }, + }, + Note: `The good example grants only the capability a debugger needs. Reach for the +narrowest grant that works: ` + "`" + `capAdd` + "`" + ` for a capability, ` + "`" + `--device` + "`" + ` in ` + "`" + `runArgs` + "`" + ` +for a device, and the docker-in-docker Feature rather than privileged mode for +nested containers.`, + }, + Check: checkNoPrivilegedContainer, } func checkNoPrivilegedContainer(ctx *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/no_seccomp_override.go b/rules/no_seccomp_override.go index 1e7fd00..fe27bb1 100644 --- a/rules/no_seccomp_override.go +++ b/rules/no_seccomp_override.go @@ -16,10 +16,41 @@ import ( var NoSeccompOverride = &linter.Rule{ ID: "no-seccomp-override", Description: `disallow overriding the container runtime's default seccomp profile via a devcontainer.json's or Feature's "securityOpt" property, or a "--security-opt seccomp=..." entry in a devcontainer.json's "runArgs"`, - Category: linter.CategorySecurity, - FileTypes: []linter.FileType{linter.Devcontainer, linter.Feature}, - Paths: []string{"/securityOpt/*", "/runArgs/*"}, - Check: checkNoSeccompOverride, + LongDescription: `The runtime's default seccomp profile blocks the syscalls containers do not need, several of which have +featured in container escapes. Pointing "seccomp" at a profile of your own replaces that default +wholesale, and a hand-written profile is rarely reviewed as carefully or updated as the kernel gains new +syscalls. Keep the default unless the workload provably needs more, and review the replacement if it +does.`, + References: []string{ + `https://containers.dev/implementors/json_reference/#general-devcontainerjson-properties`, + `https://docs.docker.com/engine/security/seccomp/`, + }, + Category: linter.CategorySecurity, + FileTypes: []linter.FileType{linter.Devcontainer, linter.Feature}, + Paths: []string{"/securityOpt/*", "/runArgs/*"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "securityOpt": ["seccomp=./seccomp.json"] +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "capAdd": ["SYS_PTRACE"] +} +`}, + }, + }, + Note: `Leaving ` + "`" + `securityOpt` + "`" + ` unset keeps the runtime's default seccomp profile, +which already allows what a development container normally does.`, + }, + Check: checkNoSeccompOverride, } func checkNoSeccompOverride(ctx *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/no_seccomp_unconfined.go b/rules/no_seccomp_unconfined.go index ddc9941..7696c96 100644 --- a/rules/no_seccomp_unconfined.go +++ b/rules/no_seccomp_unconfined.go @@ -12,10 +12,38 @@ import ( var NoSeccompUnconfined = &linter.Rule{ ID: "no-seccomp-unconfined", Description: `disallow disabling seccomp confinement via a devcontainer.json's or Feature's "securityOpt" property, or a "--security-opt seccomp=unconfined" entry in a devcontainer.json's "runArgs"`, - Category: linter.CategorySecurity, - FileTypes: []linter.FileType{linter.Devcontainer, linter.Feature}, - Paths: []string{"/securityOpt/*", "/runArgs"}, - Check: checkNoSeccompUnconfined, + LongDescription: `"seccomp=unconfined" turns off syscall filtering entirely, exposing the whole kernel API — including the +calls the default profile blocks precisely because they have been used to break out of containers. The +setting is most often copied from debugger instructions, where granting the "SYS_PTRACE" capability is +enough on current runtimes.`, + References: []string{ + `https://containers.dev/implementors/json_reference/#general-devcontainerjson-properties`, + `https://docs.docker.com/engine/security/seccomp/`, + }, + Category: linter.CategorySecurity, + FileTypes: []linter.FileType{linter.Devcontainer, linter.Feature}, + Paths: []string{"/securityOpt/*", "/runArgs"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "securityOpt": ["seccomp=unconfined"] +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "capAdd": ["SYS_PTRACE"] +} +`}, + }, + }, + }, + Check: checkNoSeccompUnconfined, } func checkNoSeccompUnconfined(ctx *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/pin_extension_version.go b/rules/pin_extension_version.go index 91c80db..57624d0 100644 --- a/rules/pin_extension_version.go +++ b/rules/pin_extension_version.go @@ -15,11 +15,47 @@ import ( var PinExtensionVersion = &linter.Rule{ ID: "pin-extension-version", Description: `disallow a "customizations.vscode.extensions" entry without an explicit pinned version`, - Category: linter.CategoryReproducibility, - FileTypes: []linter.FileType{linter.Devcontainer}, - Platforms: []linter.Platform{linter.PlatformVSCode, linter.PlatformCodespaces}, - Paths: []string{"/customizations/vscode/extensions/*"}, - Check: checkPinExtensionVersion, + LongDescription: `An extension ID on its own installs whatever the marketplace publishes at the moment the container is +created, so two developers on the same devcontainer.json can end up with different formatters, linters, or +language server versions — and an extension update can change the environment without any commit. +Appending a version ("publisher.name@1.2.3") makes the editor tooling as pinned as the rest of the image.`, + References: []string{ + `https://github.com/devcontainers/spec/blob/main/docs/specs/supporting-tools.md#visual-studio-code`, + `https://code.visualstudio.com/docs/configure/extensions/extension-marketplace`, + }, + Category: linter.CategoryReproducibility, + FileTypes: []linter.FileType{linter.Devcontainer}, + Platforms: []linter.Platform{linter.PlatformVSCode, linter.PlatformCodespaces}, + Paths: []string{"/customizations/vscode/extensions/*"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "customizations": { + "vscode": { + "extensions": ["golang.go"] + } + } +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "customizations": { + "vscode": { + "extensions": ["golang.go@0.50.0"] + } + } +} +`}, + }, + }, + }, + Check: checkPinExtensionVersion, } func checkPinExtensionVersion(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/pin_feature_version.go b/rules/pin_feature_version.go index 6535582..49bd67a 100644 --- a/rules/pin_feature_version.go +++ b/rules/pin_feature_version.go @@ -16,10 +16,42 @@ import ( var PinFeatureVersion = &linter.Rule{ ID: "pin-feature-version", Description: `disallow a Feature reference without an explicit version or with the "latest" version`, - Category: linter.CategoryReproducibility, - FileTypes: []linter.FileType{linter.Devcontainer}, - Paths: []string{"/features"}, - Check: checkPinFeatureVersion, + LongDescription: `A Feature reference with no version resolves to "latest", so the container installs whatever the Feature's +author published most recently — the tooling it sets up can change under the project without the +devcontainer.json changing at all. Features are published under their full version as well as +"major.minor" and "major" tags, so a reference can be pinned as tightly as the project wants.`, + References: []string{ + `https://containers.dev/implementors/features-distribution/#versioning`, + `https://containers.dev/implementors/features/#referencing-a-feature`, + }, + Category: linter.CategoryReproducibility, + FileTypes: []linter.FileType{linter.Devcontainer}, + Paths: []string{"/features"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io/devcontainers/features/go": {} + } +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io/devcontainers/features/go:1.3.2": {} + } +} +`}, + }, + }, + }, + Check: checkPinFeatureVersion, } func checkPinFeatureVersion(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/pin_image_digest.go b/rules/pin_image_digest.go index 434f06d..a8fcb09 100644 --- a/rules/pin_image_digest.go +++ b/rules/pin_image_digest.go @@ -21,10 +21,36 @@ var digestSuffix = regexp.MustCompile(`@[a-z0-9]+(?:[+._-][a-z0-9]+)*:[a-zA-Z0-9 var PinImageDigest = &linter.Rule{ ID: "pin-image-digest", Description: `disallow an "image" property that does not pin the image by content digest (e.g. "image@sha256:...")`, - Category: linter.CategoryReproducibility, - FileTypes: []linter.FileType{linter.Devcontainer}, - Paths: []string{"/image"}, - Check: checkPinImageDigest, + LongDescription: `A tag is a mutable pointer: the publisher can move even a fully specified one to different bits, and a +registry can serve a different image for the same tag on a different day. A digest names the content +itself, so "image@sha256:..." always resolves to the exact image the project was tested with, and the +client verifies what it pulled against it.`, + References: []string{ + `https://containers.dev/implementors/json_reference/#image-or-dockerfile-specific-properties`, + `https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests`, + }, + Category: linter.CategoryReproducibility, + FileTypes: []linter.FileType{linter.Devcontainer}, + Paths: []string{"/image"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04" +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04@sha256:2a1d1e1a4b0c3f8e5c8a1e0a6d3b7c9f4e2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b" +} +`}, + }, + }, + }, + Check: checkPinImageDigest, } func checkPinImageDigest(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/require_cap_drop_all.go b/rules/require_cap_drop_all.go index 5bd459c..e698732 100644 --- a/rules/require_cap_drop_all.go +++ b/rules/require_cap_drop_all.go @@ -12,10 +12,42 @@ import ( var RequireCapDropAll = &linter.Rule{ ID: "require-cap-drop-all", Description: `require a "--cap-drop=ALL" entry in a devcontainer.json's "runArgs", dropping every Linux capability`, - Category: linter.CategorySecurity, - FileTypes: []linter.FileType{linter.Devcontainer}, - Paths: []string{""}, - Check: checkRequireCapDropAll, + LongDescription: `Container runtimes grant a default set of capabilities that a dev container almost never uses: raw network +access, changing file ownership, or binding privileged ports. Dropping all of them and adding back only +what the workload needs ("capAdd") means a process that is compromised inherits no privilege the project +never asked for.`, + References: []string{ + `https://containers.dev/implementors/json_reference/#general-devcontainerjson-properties`, + `https://docs.docker.com/engine/security/#linux-kernel-capabilities`, + }, + Category: linter.CategorySecurity, + FileTypes: []linter.FileType{linter.Devcontainer}, + Paths: []string{""}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu" +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "runArgs": ["--cap-drop=ALL"], + "capAdd": ["CHOWN", "SETUID", "SETGID"] +} +`}, + }, + }, + Note: "`" + `runArgs` + "`" + ` is the only place this can be expressed: devcontainer.json has a +` + "`" + `capAdd` + "`" + ` property but no ` + "`" + `capDrop` + "`" + ` one, so dropping capabilities means passing +the flag to the container runtime. Add back through ` + "`" + `capAdd` + "`" + ` whatever the +workload actually needs.`, + }, + Check: checkRequireCapDropAll, } func checkRequireCapDropAll(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/require_no_new_privileges.go b/rules/require_no_new_privileges.go index ce3c718..fc65ccc 100644 --- a/rules/require_no_new_privileges.go +++ b/rules/require_no_new_privileges.go @@ -13,10 +13,37 @@ import ( var RequireNoNewPrivileges = &linter.Rule{ ID: "require-no-new-privileges", Description: `require "no-new-privileges" to be set via a devcontainer.json's "securityOpt" property, or a "--security-opt no-new-privileges..." entry in "runArgs"`, - Category: linter.CategorySecurity, - FileTypes: []linter.FileType{linter.Devcontainer}, - Paths: []string{""}, - Check: checkRequireNoNewPrivileges, + LongDescription: `Without this option a process in the container can still gain privileges it was not started with, by +executing a setuid binary — which undercuts the point of running as a non-root user. Setting it raises the +kernel's "no_new_privs" bit, which every child process inherits and none can clear, so the container's +privileges can only ever shrink.`, + References: []string{ + `https://containers.dev/implementors/json_reference/#general-devcontainerjson-properties`, + `https://docs.kernel.org/userspace-api/no_new_privs.html`, + }, + Category: linter.CategorySecurity, + FileTypes: []linter.FileType{linter.Devcontainer}, + Paths: []string{""}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu" +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "securityOpt": ["no-new-privileges"] +} +`}, + }, + }, + }, + Check: checkRequireNoNewPrivileges, } func checkRequireNoNewPrivileges(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/require_non_root.go b/rules/require_non_root.go index 02aa4b5..620a3ee 100644 --- a/rules/require_non_root.go +++ b/rules/require_non_root.go @@ -16,10 +16,41 @@ import ( var RequireNonRoot = &linter.Rule{ ID: "require-non-root", Description: `require "remoteUser" or, if unset, "containerUser" to be set to a non-root user`, - Category: linter.CategorySecurity, - FileTypes: []linter.FileType{linter.Devcontainer}, - Paths: []string{""}, - Check: checkRequireNonRoot, + LongDescription: `"remoteUser" defaults to whatever user the container runs as, which for most images is root. Everything +the developer's session drives then runs as root: lifecycle scripts, terminals, and the language servers +and build tools the editor starts, so a compromised dependency runs with full control of the container. +Naming an unprivileged user — as the specification's own images do — costs nothing and contains it.`, + References: []string{ + `https://containers.dev/implementors/json_reference/#remoteuser`, + `https://containers.dev/implementors/spec/#users`, + }, + Category: linter.CategorySecurity, + FileTypes: []linter.FileType{linter.Devcontainer}, + Paths: []string{""}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "remoteUser": "root" +} +`}, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + {Path: `devcontainer.json`, Content: `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "remoteUser": "vscode" +} +`}, + }, + }, + Note: "`" + `remoteUser` + "`" + ` is what lifecycle scripts and the editor's remote session run as, +and it wins over ` + "`" + `containerUser` + "`" + `; a rule that finds neither reports the +container's default, which is ` + "`" + `root` + "`" + ` for most images.`, + }, + Check: checkRequireNonRoot, } func checkRequireNonRoot(_ *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/undefined_template_option.go b/rules/undefined_template_option.go index 2b76fc1..8aed4aa 100644 --- a/rules/undefined_template_option.go +++ b/rules/undefined_template_option.go @@ -16,10 +16,74 @@ import ( var UndefinedTemplateOption = &linter.Rule{ ID: "undefined-template-option", Description: "disallow a `${templateOption:...}` reference to an option not declared in devcontainer-template.json", - Category: linter.CategoryCorrectness, - FileTypes: []linter.FileType{linter.Template}, - Paths: []string{""}, - Check: checkUndefinedTemplateOption, + LongDescription: `Applying a Template replaces each "${templateOption:name}" with the value the user chose for the option of +that name. A reference to an option that "options" does not declare is never prompted for, and the +reference implementation substitutes the empty string for it, so a typo silently produces an empty value +in the applied files instead of an error.`, + References: []string{ + `https://containers.dev/implementors/templates/#the-options-property`, + `https://github.com/devcontainers/cli`, + }, + Category: linter.CategoryCorrectness, + FileTypes: []linter.FileType{linter.Template}, + Paths: []string{""}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + { + Path: `devcontainer-template.json`, + Content: `{ + "id": "dotnet", + "version": "1.0.0", + "name": "C# (.NET)", + "options": { + "imageVariant": { + "type": "string", + "proposals": ["8.0", "9.0"], + "default": "9.0" + } + } +} +`, + }, + { + Path: `.devcontainer/devcontainer.json`, + Content: `{ + "image": "mcr.microsoft.com/devcontainers/dotnet:${templateOption:variant}" +} +`, + }, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + { + Path: `devcontainer-template.json`, + Content: `{ + "id": "dotnet", + "version": "1.0.0", + "name": "C# (.NET)", + "options": { + "imageVariant": { + "type": "string", + "proposals": ["8.0", "9.0"], + "default": "9.0" + } + } +} +`, + }, + { + Path: `.devcontainer/devcontainer.json`, + Content: `{ + "image": "mcr.microsoft.com/devcontainers/dotnet:${templateOption:imageVariant}" +} +`, + }, + }, + }, + }, + Check: checkUndefinedTemplateOption, } func checkUndefinedTemplateOption(ctx *linter.Context, node *linter.Node) []linter.Finding { diff --git a/rules/unused_template_option.go b/rules/unused_template_option.go index 73c0418..3cbc2f0 100644 --- a/rules/unused_template_option.go +++ b/rules/unused_template_option.go @@ -13,10 +13,73 @@ import ( var UnusedTemplateOption = &linter.Rule{ ID: "unused-template-option", Description: "disallow a Template option that no file in the Template references", - Category: linter.CategoryStyle, - FileTypes: []linter.FileType{linter.Template}, - Paths: []string{"/options"}, - Check: checkUnusedTemplateOption, + LongDescription: `An option only takes effect where a file substitutes it as "${templateOption:name}". One that nothing +references is still presented to the user when the Template is applied, so it asks a question whose +answer changes nothing — usually a leftover from a removed file or a renamed reference.`, + References: []string{ + `https://containers.dev/implementors/templates/#the-options-property`, + `https://containers.dev/implementors/templates/#option-resolution`, + }, + Category: linter.CategoryStyle, + FileTypes: []linter.FileType{linter.Template}, + Paths: []string{"/options"}, + Example: linter.Example{ + Bad: linter.Snippet{ + Files: []linter.ExampleFile{ + { + Path: `devcontainer-template.json`, + Content: `{ + "id": "dotnet", + "version": "1.0.0", + "name": "C# (.NET)", + "options": { + "imageVariant": { + "type": "string", + "proposals": ["8.0", "9.0"], + "default": "9.0" + } + } +} +`, + }, + { + Path: `.devcontainer/devcontainer.json`, + Content: `{ + "image": "mcr.microsoft.com/devcontainers/dotnet:9.0" +} +`, + }, + }, + }, + Good: linter.Snippet{ + Files: []linter.ExampleFile{ + { + Path: `devcontainer-template.json`, + Content: `{ + "id": "dotnet", + "version": "1.0.0", + "name": "C# (.NET)", + "options": { + "imageVariant": { + "type": "string", + "proposals": ["8.0", "9.0"], + "default": "9.0" + } + } +} +`, + }, + { + Path: `.devcontainer/devcontainer.json`, + Content: `{ + "image": "mcr.microsoft.com/devcontainers/dotnet:${templateOption:imageVariant}" +} +`, + }, + }, + }, + }, + Check: checkUnusedTemplateOption, } func checkUnusedTemplateOption(ctx *linter.Context, node *linter.Node) []linter.Finding {