Skip to content
Draft
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
88 changes: 45 additions & 43 deletions cmd/cli/cmd/auth.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cmd

import (
"context"
"encoding/json"
"fmt"
"os"
Expand All @@ -25,7 +24,7 @@ var loginCmd = &cobra.Command{
Use: "login",
Short: "Log in to Nullify",
Long: "Authenticate with your Nullify instance. Opens your browser to log in with your identity provider.",
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
ctx := setupLogger(cmd.Context())
defer logger.Close(ctx)

Expand All @@ -43,74 +42,77 @@ var loginCmd = &cobra.Command{
}
}

// If still no host, prompt
// If still no host, prompt (only when running interactively).
if loginHost == "" {
if !stdinIsTTY() {
return fmt.Errorf("not a terminal; pass --host <your-instance>.nullify.ai")
}
fmt.Print("Enter your Nullify instance (e.g., acme.nullify.ai): ")
_, _ = fmt.Scanln(&loginHost)
}

sanitizedHost, err := lib.SanitizeNullifyHost(loginHost)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: invalid host %q - must be in the format <your-instance>.nullify.ai\n", loginHost)
os.Exit(1)
return fmt.Errorf("invalid host %q - must be in the format <your-instance>.nullify.ai", loginHost)
}

err = auth.Login(ctx, sanitizedHost)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: login failed: %v\n", err)
os.Exit(1)
if err := auth.Login(ctx, sanitizedHost); err != nil {
return fmt.Errorf("login failed: %w", err)
}
return nil
},
}

var logoutCmd = &cobra.Command{
Use: "logout",
Short: "Log out of Nullify",
Long: "Clear stored credentials for the current or specified host.",
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
ctx := setupLogger(cmd.Context())
defer logger.Close(ctx)

logoutHost := resolveHostForAuth(ctx)

err := auth.Logout(logoutHost)
logoutHost, err := resolveHostForAuth()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: logout failed: %v\n", err)
os.Exit(1)
return err
}

if err := auth.Logout(logoutHost); err != nil {
return fmt.Errorf("logout failed: %w", err)
}

fmt.Printf("Logged out from %s\n", logoutHost)
return nil
},
}

var statusCmd = &cobra.Command{
Use: "status",
Short: "Show authentication status",
Long: "Display the current authentication state including host, user, and token expiry.",
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := auth.LoadConfig()
if err != nil {
fmt.Println("Not configured. Run 'nullify auth login --host <your-instance>.nullify.ai' to get started.")
return
return nil
}

if cfg.Host == "" {
fmt.Println("No host configured. Run 'nullify auth login --host <your-instance>.nullify.ai'")
return
return nil
}

fmt.Printf("Host: %s\n", cfg.Host)

creds, err := auth.LoadCredentials()
if err != nil {
fmt.Println("Status: not authenticated")
return
return nil
}

hostCreds, ok := creds[auth.CredentialKey(cfg.Host)]
if !ok {
fmt.Println("Status: not authenticated")
return
return nil
}

if hostCreds.ExpiresAt > 0 {
Expand All @@ -123,34 +125,38 @@ var statusCmd = &cobra.Command{
} else {
fmt.Println("Status: authenticated")
}
return nil
},
}

var tokenCmd = &cobra.Command{
Use: "token",
Short: "Print access token to stdout",
Long: "Print the current access token. Useful for piping to other tools.",
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
ctx := setupLogger(cmd.Context())
defer logger.Close(ctx)

hostForToken := resolveHostForAuth(ctx)
hostForToken, err := resolveHostForAuth()
if err != nil {
return err
}

token, err := auth.GetValidToken(ctx, hostForToken)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
return err
}

fmt.Println(token)
return nil
},
}

var switchCmd = &cobra.Command{
Use: "switch",
Short: "Switch between configured hosts",
Long: "Switch the default host when multiple Nullify instances are configured.",
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
switchHost, _ := cmd.Flags().GetString("host")
if switchHost == "" && host != "" {
switchHost = host
Expand All @@ -161,7 +167,7 @@ var switchCmd = &cobra.Command{
creds, err := auth.LoadCredentials()
if err != nil || len(creds) == 0 {
fmt.Println("No configured hosts. Run 'nullify auth login --host <your-instance>.nullify.ai'")
return
return nil
}

cfg, _ := auth.LoadConfig()
Expand All @@ -174,13 +180,12 @@ var switchCmd = &cobra.Command{
fmt.Printf("%s%s\n", marker, h)
}
fmt.Println("\nUse 'nullify auth switch --host <host>' to switch.")
return
return nil
}

sanitizedHost, err := lib.SanitizeNullifyHost(switchHost)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: invalid host %q\n", switchHost)
os.Exit(1)
return fmt.Errorf("invalid host %q", switchHost)
}

cfg, err := auth.LoadConfig()
Expand All @@ -189,29 +194,29 @@ var switchCmd = &cobra.Command{
}
cfg.Host = sanitizedHost

err = auth.SaveConfig(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to save config: %v\n", err)
os.Exit(1)
if err := auth.SaveConfig(cfg); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}

fmt.Printf("Switched to %s\n", sanitizedHost)
return nil
},
}

var configShowCmd = &cobra.Command{
Use: "config",
Short: "Show current configuration",
Long: "Display the current CLI configuration as JSON.",
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := auth.LoadConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "No config found. Run 'nullify init' to set up.\n")
return
return nil
}

data, _ := json.MarshalIndent(cfg, "", " ")
fmt.Println(string(data))
return nil
},
}

Expand All @@ -225,25 +230,22 @@ func init() {
authCmd.AddCommand(configShowCmd)
}

func resolveHostForAuth(ctx context.Context) string {
func resolveHostForAuth() (string, error) {
if host != "" {
sanitized, err := lib.SanitizeNullifyHost(host)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: invalid host %q\n", host)
os.Exit(1)
return "", fmt.Errorf("invalid host %q", host)
}
return sanitized
return sanitized, nil
}

cfg, err := auth.LoadConfig()
if err == nil && cfg.Host != "" {
sanitized, sErr := lib.SanitizeNullifyHost(cfg.Host)
if sErr == nil {
return sanitized
return sanitized, nil
}
}

fmt.Fprintln(os.Stderr, "Error: no host configured. Use --host or run 'nullify auth login'")
os.Exit(1)
return ""
return "", fmt.Errorf("no host configured. Use --host or run 'nullify auth login'")
}
74 changes: 26 additions & 48 deletions cmd/cli/cmd/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@ package cmd

import (
"encoding/json"
"errors"
"fmt"
"os"
"sync"
"sync/atomic"

"github.com/nullify-platform/cli/internal/auth"
"github.com/nullify-platform/cli/internal/client"
"github.com/nullify-platform/cli/internal/lib"
"github.com/nullify-platform/cli/internal/logger"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -41,30 +38,22 @@ Exit codes:

# Check a specific repo
nullify ci gate --repo my-org/my-repo`,
Run: func(cmd *cobra.Command, args []string) {
// SilenceErrors: this command writes its own gate-failure summary to
// stdout and must not have cobra echo the sentinel error to stderr.
// Auth/network errors are printed explicitly below to preserve the
// original stderr output.
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := setupLogger(cmd.Context())
defer logger.Close(ctx)

ciHost := resolveHost(ctx)
token, err := lib.GetNullifyToken(ctx, ciHost, nullifyToken, githubToken)
authCtx, err := resolveCommandAuth(ctx)
if err != nil {
if errors.Is(err, lib.ErrNoToken) {
fmt.Fprintf(os.Stderr, "Error: not authenticated. Run 'nullify auth login' first.\n")
} else {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
}
os.Exit(ExitAuthError)
}

nullifyClient := client.NewNullifyClient(ciHost, token)

creds, err := auth.LoadCredentials()
queryParams := map[string]string{}
if err == nil {
if hostCreds, ok := creds[auth.CredentialKey(ciHost)]; ok && hostCreds.QueryParameters != nil {
queryParams = hostCreds.QueryParameters
}
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return err
}
nullifyClient := authCtx.Client()
queryParams := authCtx.QueryParams

severityThreshold, _ := cmd.Flags().GetString("severity-threshold")
findingType, _ := cmd.Flags().GetString("type")
Expand All @@ -79,8 +68,9 @@ Exit codes:
}
}
if !validThreshold {
fmt.Fprintf(os.Stderr, "Error: invalid --severity-threshold %q. Valid values: critical, high, medium, low\n", severityThreshold)
os.Exit(1)
err := fmt.Errorf("invalid --severity-threshold %q. Valid values: critical, high, medium, low", severityThreshold)
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return withExitCode(1, err)
}

if repo == "" {
Expand Down Expand Up @@ -138,16 +128,18 @@ Exit codes:
_ = g.Wait()

if apiErrors > 0 && apiErrors == totalRequests {
fmt.Fprintf(os.Stderr, "Error: all API requests failed, cannot determine gate status\n")
os.Exit(ExitNetworkError)
err := fmt.Errorf("all API requests failed, cannot determine gate status")
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return withExitCode(ExitNetworkError, err)
}

if totalFindings > 0 {
fmt.Printf("\nGate failed: %d findings at or above %s severity\n", totalFindings, severityThreshold)
os.Exit(ExitFindings)
return withExitCode(ExitFindings, fmt.Errorf("gate failed: %d findings at or above %s severity", totalFindings, severityThreshold))
}

fmt.Println("Gate passed: no findings above threshold")
return nil
},
}

Expand All @@ -157,30 +149,16 @@ var ciReportCmd = &cobra.Command{
Long: "Output a markdown summary of security findings suitable for PR comments. Shows counts by type and severity.",
Example: ` nullify ci report
nullify ci report --repo my-org/my-repo`,
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
ctx := setupLogger(cmd.Context())
defer logger.Close(ctx)

ciHost := resolveHost(ctx)
token, err := lib.GetNullifyToken(ctx, ciHost, nullifyToken, githubToken)
authCtx, err := resolveCommandAuth(ctx)
if err != nil {
if errors.Is(err, lib.ErrNoToken) {
fmt.Fprintf(os.Stderr, "Error: not authenticated. Run 'nullify auth login' first.\n")
} else {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
}
os.Exit(ExitAuthError)
}

nullifyClient := client.NewNullifyClient(ciHost, token)

creds, err := auth.LoadCredentials()
queryParams := map[string]string{}
if err == nil {
if hostCreds, ok := creds[auth.CredentialKey(ciHost)]; ok && hostCreds.QueryParameters != nil {
queryParams = hostCreds.QueryParameters
}
return err
}
nullifyClient := authCtx.Client()
queryParams := authCtx.QueryParams

repo, _ := cmd.Flags().GetString("repo")
if repo == "" {
Expand Down Expand Up @@ -235,8 +213,7 @@ var ciReportCmd = &cobra.Command{
_ = g.Wait()

if successCount == 0 {
fmt.Fprintln(os.Stderr, "Error: all API requests failed, cannot generate report")
os.Exit(ExitNetworkError)
return networkError("all API requests failed, cannot generate report")
}
if apiErrors > 0 {
fmt.Fprintf(os.Stderr, "Warning: %d API requests failed while generating the report\n", apiErrors)
Expand All @@ -255,6 +232,7 @@ var ciReportCmd = &cobra.Command{

fmt.Println()
fmt.Println("*Generated by [Nullify CLI](https://github.com/nullify-platform/cli)*")
return nil
},
}

Expand Down
Loading