Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25.5'
go-version: '1.26.3'

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v7
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
if: ${{ steps.release.outputs.release_created }}
uses: actions/setup-go@v5
with:
go-version: '1.25.5'
go-version: '1.26.3'

- name: Run GoReleaser
if: ${{ steps.release.outputs.release_created }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25.5'
go-version: '1.26.3'

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25.5'
go-version: '1.26.3'

- name: Run unit tests
run: go test ./... -cover
Expand Down
17 changes: 12 additions & 5 deletions cmd/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ var (
strictMode bool

// Ignore flags.
ignoreDomains []string
ignorePatterns []string
ignoreRegex []string
showIgnored bool
noConfig bool
ignoreDomains []string
ignorePatterns []string
ignoreRegex []string
showIgnored bool
noConfig bool
allowPrivateHosts bool
)

// checkCmd represents the check command.
Expand Down Expand Up @@ -144,6 +145,12 @@ func init() {
"Show which URLs were ignored and why")
checkCmd.Flags().BoolVar(&noConfig, "no-config", false,
"Skip loading .gonerc.yaml config file")

// Security options
checkCmd.Flags().BoolVar(&allowPrivateHosts, "allow-private-hosts", false,
"Allow requests to loopback, private, link-local and reserved IP "+
"ranges. Default is to block them to prevent SSRF when scanning "+
"untrusted documents.")
}

// runCheck is the main entry point for the check command.
Expand Down
75 changes: 40 additions & 35 deletions cmd/check_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,24 @@ import (
)

type checkOptions struct {
OutputFormat string
OutputFile string
Concurrency int
Timeout int
Retries int
ShowAlive bool
ShowWarnings bool
ShowDead bool
ShowAll bool
ShowStats bool
FileTypes []string
StrictMode bool
IgnoreDomains []string
IgnorePatterns []string
IgnoreRegex []string
ShowIgnored bool
NoConfig bool
OutputFormat string
OutputFile string
Concurrency int
Timeout int
Retries int
ShowAlive bool
ShowWarnings bool
ShowDead bool
ShowAll bool
ShowStats bool
FileTypes []string
StrictMode bool
IgnoreDomains []string
IgnorePatterns []string
IgnoreRegex []string
ShowIgnored bool
NoConfig bool
AllowPrivateHosts bool
}

type checkRunner struct {
Expand All @@ -46,23 +47,24 @@ func newCheckRunner(opts checkOptions, env CommandEnv, streams IOStreams) *check

func currentCheckOptions() checkOptions {
return checkOptions{
OutputFormat: outputFormat,
OutputFile: outputFile,
Concurrency: concurrency,
Timeout: timeout,
Retries: retries,
ShowAlive: showAlive,
ShowWarnings: showWarnings,
ShowDead: showDead,
ShowAll: showAll,
ShowStats: showStats,
FileTypes: append([]string{}, fileTypes...),
StrictMode: strictMode,
IgnoreDomains: append([]string{}, ignoreDomains...),
IgnorePatterns: append([]string{}, ignorePatterns...),
IgnoreRegex: append([]string{}, ignoreRegex...),
ShowIgnored: showIgnored,
NoConfig: noConfig,
OutputFormat: outputFormat,
OutputFile: outputFile,
Concurrency: concurrency,
Timeout: timeout,
Retries: retries,
ShowAlive: showAlive,
ShowWarnings: showWarnings,
ShowDead: showDead,
ShowAll: showAll,
ShowStats: showStats,
FileTypes: append([]string{}, fileTypes...),
StrictMode: strictMode,
IgnoreDomains: append([]string{}, ignoreDomains...),
IgnorePatterns: append([]string{}, ignorePatterns...),
IgnoreRegex: append([]string{}, ignoreRegex...),
ShowIgnored: showIgnored,
NoConfig: noConfig,
AllowPrivateHosts: allowPrivateHosts,
}
}

Expand Down Expand Up @@ -244,7 +246,10 @@ func (r *checkRunner) checkLinksWithConfig(
links []checker.Link, cfg *LoadedConfig, perf *stats.Stats,
) ([]checker.Result, checker.Summary) {
perf.StartCheck()
c := r.env.NewChecker(cfg.BuildCheckerOptions(r.opts.Concurrency, r.opts.Timeout, r.opts.Retries))
c := r.env.NewChecker(cfg.BuildCheckerOptions(
r.opts.Concurrency, r.opts.Timeout, r.opts.Retries,
r.opts.AllowPrivateHosts,
))
results := c.CheckAll(links)
summary := checker.Summarize(results)
perf.EndCheck()
Expand Down
2 changes: 2 additions & 0 deletions cmd/e2e_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ func BenchmarkPipeline_FullCheck(b *testing.B) {

c := checker.New(
checker.DefaultOptions().
WithAllowPrivateHosts(true).
WithConcurrency(16).
WithTimeout(2 * time.Second).
WithMaxRetries(0),
Expand Down Expand Up @@ -135,6 +136,7 @@ func BenchmarkPipeline_FixDryRun(b *testing.B) {

c := checker.New(
checker.DefaultOptions().
WithAllowPrivateHosts(true).
WithConcurrency(16).
WithTimeout(2 * time.Second).
WithMaxRetries(0),
Expand Down
14 changes: 10 additions & 4 deletions cmd/fix.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ var (
fixStrictMode bool

// Ignore flags (shared with check).
fixIgnoreDomains []string
fixIgnorePatterns []string
fixIgnoreRegex []string
fixNoConfig bool
fixIgnoreDomains []string
fixIgnorePatterns []string
fixIgnoreRegex []string
fixNoConfig bool
fixAllowPrivateHosts bool
)

// fixCmd represents the fix command.
Expand Down Expand Up @@ -100,6 +101,11 @@ func init() {
"Regex patterns to ignore (can be repeated)")
fixCmd.Flags().BoolVar(&fixNoConfig, "no-config", false,
"Skip loading .gonerc.yaml config file")

// Security options
fixCmd.Flags().BoolVar(&fixAllowPrivateHosts, "allow-private-hosts", false,
"Allow requests to loopback, private, link-local and reserved IP "+
"ranges. Default is to block them to prevent SSRF.")
}

// runFix is the main entry point for the fix command.
Expand Down
55 changes: 30 additions & 25 deletions cmd/fix_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@ import (
)

type fixOptions struct {
Yes bool
DryRun bool
Concurrency int
Timeout int
Retries int
ShowStats bool
FileTypes []string
StrictMode bool
IgnoreDomains []string
IgnorePatterns []string
IgnoreRegex []string
NoConfig bool
Yes bool
DryRun bool
Concurrency int
Timeout int
Retries int
ShowStats bool
FileTypes []string
StrictMode bool
IgnoreDomains []string
IgnorePatterns []string
IgnoreRegex []string
NoConfig bool
AllowPrivateHosts bool
}

type fixRunner struct {
Expand All @@ -40,18 +41,19 @@ func newFixRunner(opts fixOptions, env CommandEnv, streams IOStreams) *fixRunner

func currentFixOptions() fixOptions {
return fixOptions{
Yes: fixYes,
DryRun: fixDryRun,
Concurrency: fixConcurrency,
Timeout: fixTimeout,
Retries: fixRetries,
ShowStats: fixShowStats,
FileTypes: append([]string{}, fixFileTypes...),
StrictMode: fixStrictMode,
IgnoreDomains: append([]string{}, fixIgnoreDomains...),
IgnorePatterns: append([]string{}, fixIgnorePatterns...),
IgnoreRegex: append([]string{}, fixIgnoreRegex...),
NoConfig: fixNoConfig,
Yes: fixYes,
DryRun: fixDryRun,
Concurrency: fixConcurrency,
Timeout: fixTimeout,
Retries: fixRetries,
ShowStats: fixShowStats,
FileTypes: append([]string{}, fixFileTypes...),
StrictMode: fixStrictMode,
IgnoreDomains: append([]string{}, fixIgnoreDomains...),
IgnorePatterns: append([]string{}, fixIgnorePatterns...),
IgnoreRegex: append([]string{}, fixIgnoreRegex...),
NoConfig: fixNoConfig,
AllowPrivateHosts: fixAllowPrivateHosts,
}
}

Expand Down Expand Up @@ -126,7 +128,10 @@ func (r *fixRunner) Run(args []string) int {

perf.StartCheck()
results := r.env.NewChecker(
loadedCfg.BuildCheckerOptions(r.opts.Concurrency, r.opts.Timeout, r.opts.Retries),
loadedCfg.BuildCheckerOptions(
r.opts.Concurrency, r.opts.Timeout, r.opts.Retries,
r.opts.AllowPrivateHosts,
),
).CheckAll(links)
perf.EndCheck()

Expand Down
19 changes: 17 additions & 2 deletions cmd/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ func (lc *LoadedConfig) GetTimeout(cliValue, defaultValue int) int {
return defaultValue
}

// GetAllowPrivateHosts returns the effective allow-private-hosts flag.
// CLI true overrides config; otherwise the config value is used.
func (lc *LoadedConfig) GetAllowPrivateHosts(cliValue bool) bool {
if cliValue {
return true
}
return lc.cfg.Check.AllowPrivateHosts
}

// GetRetries returns the effective retry count.
// CLI overrides config if it differs from the default.
func (lc *LoadedConfig) GetRetries(cliValue, defaultValue int) int {
Expand Down Expand Up @@ -161,13 +170,19 @@ func (lc *LoadedConfig) GetShowStats(cliValue bool) bool {
}

// BuildCheckerOptions creates checker.Options from config and CLI values.
func (lc *LoadedConfig) BuildCheckerOptions(cliConcurrency, cliTimeout, cliRetries int) checker.Options {
// cliAllowPrivate reflects the --allow-private-hosts flag; when true the
// checker is permitted to contact loopback, private, and reserved IPs.
func (lc *LoadedConfig) BuildCheckerOptions(
cliConcurrency, cliTimeout, cliRetries int,
cliAllowPrivate bool,
) checker.Options {
defaultOpts := checker.DefaultOptions()

return defaultOpts.
WithConcurrency(lc.GetConcurrency(cliConcurrency, checker.DefaultConcurrency)).
WithTimeout(time.Duration(lc.GetTimeout(cliTimeout, int(checker.DefaultTimeout.Seconds()))) * time.Second).
WithMaxRetries(lc.GetRetries(cliRetries, checker.DefaultMaxRetries))
WithMaxRetries(lc.GetRetries(cliRetries, checker.DefaultMaxRetries)).
WithAllowPrivateHosts(lc.GetAllowPrivateHosts(cliAllowPrivate))
}

// BuildScanOptions creates scanner.ScanOptions from config and path.
Expand Down
2 changes: 2 additions & 0 deletions cmd/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,12 @@ func TestLoadedConfig_GettersAndBuilders(t *testing.T) {
checker.DefaultConcurrency,
int(checker.DefaultTimeout.Seconds()),
checker.DefaultMaxRetries,
false,
)
assert.Equal(t, 12, opts.Concurrency)
assert.Equal(t, 7*time.Second, opts.Timeout)
assert.Equal(t, 4, opts.MaxRetries)
assert.False(t, opts.AllowPrivateHosts)

scanOpts := loaded.BuildScanOptions("/tmp/docs", []string{"md"}, []string{"md"})
assert.Equal(t, "/tmp/docs", scanOpts.Root)
Expand Down
6 changes: 3 additions & 3 deletions cmd/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func TestCheck_WritesOutputFile(t *testing.T) {
))

reportPath := filepath.Join(tmpDir, "report.json")
result := runGone(t, tmpDir, "check", ".", "--output", reportPath, "--no-config")
result := runGone(t, tmpDir, "check", ".", "--output", reportPath, "--no-config", "--allow-private-hosts")
require.Equal(t, 0, result.exitCode, result.stderr)
assert.Contains(t, result.stdout, "Wrote report to")

Expand Down Expand Up @@ -179,7 +179,7 @@ func TestFix_Yes_UpdatesRedirectsAcrossFileTypes(t *testing.T) {
0o644,
))

result := runGone(t, tmpDir, "fix", ".", "--yes", "--types=md,json", "--no-config")
result := runGone(t, tmpDir, "fix", ".", "--yes", "--types=md,json", "--no-config", "--allow-private-hosts")
require.Equal(t, 0, result.exitCode, result.stderr)
assert.Contains(t, result.stdout, "Found 2 file(s) of type(s): md, json")
assert.Contains(t, result.stdout, "Fixed 2 redirect(s) across 2 file(s):")
Expand Down Expand Up @@ -218,7 +218,7 @@ func TestFix_InteractiveScriptedInput(t *testing.T) {
filePath := filepath.Join(tmpDir, "README.md")
require.NoError(t, os.WriteFile(filePath, []byte("[docs]("+oldURL+")\n"), 0o644))

result := runGoneWithInput(t, tmpDir, "?\ny\n", "fix", ".", "--types=md", "--no-config")
result := runGoneWithInput(t, tmpDir, "?\ny\n", "fix", ".", "--types=md", "--no-config", "--allow-private-hosts")
require.Equal(t, 0, result.exitCode, result.stderr)
assert.Contains(t, result.stdout, "Interactive mode options:")
assert.Contains(t, result.stdout, "Fixed 1 redirect(s) in README.md")
Expand Down
14 changes: 10 additions & 4 deletions cmd/interactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ var (
iFileTypes []string
iStrictMode bool

iIgnoreDomains []string
iIgnorePatterns []string
iIgnoreRegex []string
iNoConfig bool
iIgnoreDomains []string
iIgnorePatterns []string
iIgnoreRegex []string
iNoConfig bool
iAllowPrivateHosts bool
)

// interactiveCmd represents the interactive command.
Expand Down Expand Up @@ -64,6 +65,11 @@ func init() {
"Regex patterns to ignore (can be repeated)")
interactiveCmd.Flags().BoolVar(&iNoConfig, "no-config", false,
"Skip loading .gonerc.yaml config file")

// Security options
interactiveCmd.Flags().BoolVar(&iAllowPrivateHosts, "allow-private-hosts", false,
"Allow requests to loopback, private, link-local and reserved IP "+
"ranges. Default is to block them to prevent SSRF.")
}

// runInteractive launches the interactive TUI for link checking.
Expand Down
Loading
Loading