Skip to content

fix: comprehensive code review fixes across security, correctness, and robustness#131

Merged
tim-thacker-nullify merged 10 commits intomainfrom
feat/comprehensive-improvements
Mar 10, 2026
Merged

fix: comprehensive code review fixes across security, correctness, and robustness#131
tim-thacker-nullify merged 10 commits intomainfrom
feat/comprehensive-improvements

Conversation

@tim-thacker-nullify
Copy link
Copy Markdown
Member

@tim-thacker-nullify tim-thacker-nullify commented Mar 1, 2026

Summary

Addresses 34 issues identified in a senior-level code review of the CLI repository (5 critical, 6 high, 11 medium, 7 low, 5 nit). Key highlights:

Critical & Security Fixes

  • Fix broken refresh token flow: Send refresh token as HTTP cookie instead of query parameter to match backend (security-droid) expectation
  • Fix GITHUB_ACTION_REPOSITORYGITHUB_REPOSITORY: The old env var name is non-standard and not set by GitHub Actions
  • Add url.PathEscape for finding IDs in all URL path interpolations across MCP tools and CLI commands, preventing path traversal
  • Replace bare http.Get with http.NewRequestWithContext for proper timeout and context cancellation in GitHub token exchange
  • URL-encode GitHub token and owner in token exchange request
  • Fix retry transport to respect context cancellation via select on ctx.Done()
  • Fix file handle leak in openapi.go (missing defer Close)
  • Add severity-threshold validation in ci gate to reject invalid values

Correctness Fixes

  • SanitizeNullifyHost now delegates to ParseCustomerDomain, accepting acme, acme.nullify.ai, and api.acme.nullify.ai formats (strips path/query, validates hostname chars)
  • Fix openapi.go nil, nil return → descriptive error
  • Fix ci report limit from 1 → 1000 for accurate finding counts
  • Mark pentest --spec-path as required flag (removes manual check)
  • Add missing scanner types (pentest, bughunt, cspm) to MCP get_findings_for_repo and get_critical_path

Exit Codes & UX

  • Consistent exit codes across all commands: ExitAuthError(2), ExitNetworkError(3), ExitFindings(1) — CI pipelines can now distinguish auth failures from findings
  • Fix auth token to print trailing newline (was breaking piping)
  • Fix "1 findings" → "1 finding" singular form
  • Add periodic "still waiting..." message during login (every 30s)
  • Improve auth config error message when no config exists

Architecture & Code Quality

  • Deduplicate buildQueryString in MCP package → delegates to lib.BuildQueryString
  • Add ClientOption/WithHTTPClient to generated API client; wire retry transport into it
  • Export NewRetryTransport for reuse across client types
  • Parallelize status command scanner queries with errgroup
  • Use promptResult helper consistently in all MCP prompts
  • Fix wizard to use absolute paths via os.Getwd() instead of relative paths
  • Handle output.Print errors with stderr fallback
  • Remove unused --auth-config global flag (pentest-specific flag remains)
  • Clear stale Token field on refreshing transport client
  • Log warning when NULLIFY_HOST env var is invalid

Files Changed (25)

internal/auth/login.go, internal/lib/get_token.go, internal/lib/urls.go, internal/lib/openapi.go, internal/client/retry.go, internal/client/client.go, internal/client/refreshing_transport.go, internal/api/client.go, internal/mcp/server.go, internal/mcp/tools_unified.go, internal/mcp/tools_composite.go, internal/mcp/prompts.go, internal/wizard/steps.go, cmd/cli/cmd/root.go, cmd/cli/cmd/ci.go, cmd/cli/cmd/status.go, cmd/cli/cmd/findings.go, cmd/cli/cmd/fix.go, cmd/cli/cmd/auth.go, cmd/cli/cmd/chat.go, cmd/cli/cmd/mcp.go, cmd/cli/cmd/pentest.go, cmd/cli/cmd/repos.go, cmd/cli/cmd/status_test.go, scripts/generate/main.go

Test plan

  • go build ./... — compiles cleanly
  • go vet ./... — no warnings
  • go test ./... — all tests pass (updated test expectations for singular form and new URL parsing behavior)
  • Manual: nullify auth login --host acme accepts short-form host
  • Manual: nullify ci gate --severity-threshold invalid rejects with error
  • Manual: nullify auth token outputs trailing newline
  • CI: verify refresh token flow works end-to-end

🤖 Generated with Claude Code

@tim-thacker-nullify tim-thacker-nullify added the minor Minor version updates (features) label Mar 1, 2026
Copy link
Copy Markdown
Member

@jonathanlam jonathanlam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conflict

@tim-thacker-nullify tim-thacker-nullify changed the title feat: comprehensive CLI improvements across security, MCP, UX, HTTP, and distribution fix: comprehensive code review fixes across security, correctness, and robustness Mar 2, 2026
@nullify-reviewer
Copy link
Copy Markdown

Code review

Found 3 issues:

  1. time.After inside waitLoop resets on every ticker tick — the 10-minute timeout never fires

time.After(10 * time.Minute) is evaluated on every iteration of the for { select { ... } } loop. The 30-second ticker.C case fires every 30 seconds, causing a new time.After timer to be created each time. This means the effective timeout is reset every 30 seconds and will never fire, leaving authentication waiting indefinitely. The fix is to create the timer once outside the loop: timeout := time.NewTimer(10 * time.Minute).

cli/internal/auth/login.go

Lines 118 to 136 in 731fd38

ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
var sessionID string
waitLoop:
for {
select {
case sessionID = <-sessionCh:
break waitLoop
case err := <-errCh:
return fmt.Errorf("local server error: %w", err)
case <-ctx.Done():
return fmt.Errorf("authentication cancelled")
case <-time.After(10 * time.Minute):
return fmt.Errorf("authentication timed out — the session has expired")
case <-ticker.C:
fmt.Println("Still waiting for authentication...")
}
}

  1. resolveHost config-file path skips SanitizeNullifyHost, causing inconsistent host format

Paths 1 (flag) and 2 (env var) in resolveHost both call lib.SanitizeNullifyHost, which now prepends api. to bare hosts (e.g. acme.nullify.aiapi.acme.nullify.ai). Path 3 (config file) returns cfg.Host directly without sanitizing. This means the same host value resolves differently depending on how it was provided: --host acme.nullify.ai yields api.acme.nullify.ai, but a config file containing acme.nullify.ai yields the bare form. The inconsistency causes lookup mismatches in GetValidToken (keyed by host) and sends HTTP requests to the wrong host in authTransport.

cli/cmd/cli/cmd/root.go

Lines 160 to 164 in 731fd38

// 3. Read from config file
cfg, err := auth.LoadConfig()
if err == nil && cfg.Host != "" {
return cfg.Host
}

  1. SanitizeNullifyHost semantic change breaks existing credentials for users with pre-existing configs

SanitizeNullifyHost now delegates to ParseCustomerDomain, which adds an api. prefix where it previously did not (e.g. acme.nullify.ai now becomes api.acme.nullify.ai). Credentials saved by prior versions are keyed under "acme.nullify.ai" in the credentials file. After this change, GetValidToken is called with "api.acme.nullify.ai" (the newly sanitized form), which finds no match and returns "not authenticated", silently breaking authentication for any existing user whose credentials were stored under the bare-host key.

cli/internal/lib/urls.go

Lines 51 to 53 in 731fd38

func SanitizeNullifyHost(nullifyHost string) (string, error) {
return ParseCustomerDomain(nullifyHost)
}

Generated by the Platform-Tools/code-review lambda using Claude Code Opus 4.6.

tim-thacker-nullify and others added 7 commits March 6, 2026 09:11
…and distribution

Phase 1 (Security): Redact token from debug logs, add HTTP timeouts,
add 10MB response body limits, fix remediate_finding silent error bug.

Phase 2 (MCP): Add unified abstract tools (search/get/triage/ticket/events/fix),
ToolSet flag (default/all/minimal/findings/admin), 5 new prompts, 2 new resources.

Phase 3 (UX): Move generated commands under `nullify api`, add new commands
(scan, fix, open, repos, whoami, version), concurrent API calls via errgroup,
output consistency with --output flag, --quiet/--no-color flags, help examples.

Phase 4 (HTTP): User-Agent header, retry with exponential backoff, fix env var
precedence, refreshing auth transport for MCP sessions, shared apierror package,
distinct exit codes.

Phase 5 (Distribution): Homebrew tap config, `nullify update` command, SARIF
output format, enhanced `nullify version` with build metadata.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Buffer request body in retry transport to prevent empty bodies on retry
- Clone request in RoundTrip to avoid mutating the original (both transports)
- Fix generator template missing api. prefix logic for host
- Strip api. prefix in open.go to navigate to dashboard, not API
- Remove unnecessary mutex in findings.go (goroutines write to distinct indices)
- Use DoPost instead of DoGet for mutation operations in fix.go
- Fix Content-Type check in apierror to use HasPrefix for charset variants
- Sort finding type names for deterministic MCP tool enum ordering
- Parallelize ci report API calls with errgroup
- Use structured exit codes (ExitAuthError) instead of bare os.Exit(1)
- Remove misleading _ = token in mcp.go, validate auth inline

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…obustness

Critical fixes:
- Fix broken refresh token flow: send token as HTTP cookie instead of query param
  to match backend expectation (security-droid reads from cookie)
- Fix GITHUB_ACTION_REPOSITORY → GITHUB_REPOSITORY (non-standard env var)
- Add url.PathEscape for finding IDs in all URL path interpolations (MCP, CLI)
- Replace bare http.Get with http.NewRequestWithContext for proper timeout/context
- URL-encode GitHub token and owner in token exchange request

Security & correctness:
- Fix retry transport to respect context cancellation (select on ctx.Done)
- Fix file handle leak in openapi.go (missing defer Close)
- Fix openapi.go nil,nil return to return descriptive error
- SanitizeNullifyHost now delegates to ParseCustomerDomain, accepting
  'acme', 'acme.nullify.ai', and 'api.acme.nullify.ai' formats
- Strip path/query from host input, validate hostname characters
- Add severity-threshold validation in ci gate command
- Mark pentest --spec-path as required flag

Exit codes & UX:
- Use ExitAuthError(2) for auth failures, ExitNetworkError(3) for API errors,
  ExitFindings(1) for gate failures across all commands
- Fix ci report to use limit=1000 for accurate counts (was limit=1)
- Fix "1 findings" → "1 finding" singular form in status output
- Fix auth token command to print trailing newline
- Add periodic "still waiting" message during login authentication
- Improve auth config error message

Architecture & deduplication:
- Deduplicate buildQueryString in MCP package to use lib.BuildQueryString
- Add ClientOption/WithHTTPClient to generated API client for retry support
- Export NewRetryTransport and wire retry into generated API client
- Remove unused --auth-config global flag
- Parallelize status command scanner queries with errgroup
- Add missing scanner types (pentest, bughunt, cspm) to MCP composite tools
- Use promptResult helper consistently in MCP prompts
- Fix wizard to use absolute paths via os.Getwd()
- Handle output.Print errors with stderr fallback
- Clear stale Token field on refreshing transport client
- Log warning when NULLIFY_HOST env var is invalid

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The status command now directly imports errgroup from golang.org/x/sync.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…bugs

- Move time.After(10min) outside for/select loop so it fires correctly
- Sanitize config file host through SanitizeNullifyHost in resolveHost
- Normalize credential keys to bare form (strip api. prefix) at save/lookup
- Fix SanitizeNullifyHost to return bare host form consistently

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tim-thacker-nullify and others added 3 commits March 9, 2026 22:36
- Use cmd.Context() instead of context.Background() in update command
- Accept context.Context param in setupLogger() so cobra commands pass
  their own context instead of always creating a new background context
- Delete unused scan command that only printed help text
- Add credential key migration in LoadCredentials() to normalize old
  "api." prefixed keys to bare form, preventing auth failures after
  host sanitization changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Check io.Copy return value in local_scan.go to fix golangci-lint errcheck
- Use auth.CredentialKey() for credential map lookups in all commands
  (chat, mcp, status, whoami, fix, ci, findings) to match the normalized
  key format used when saving credentials

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tim-thacker-nullify tim-thacker-nullify requested a review from dnjg March 9, 2026 23:28
@tim-thacker-nullify tim-thacker-nullify merged commit 5532faa into main Mar 10, 2026
2 checks passed
@tim-thacker-nullify tim-thacker-nullify deleted the feat/comprehensive-improvements branch March 10, 2026 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor Minor version updates (features) nul-ai-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants