Skip to content
Open
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
4 changes: 2 additions & 2 deletions apps/openant-cli/cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
var analyzeCmd = &cobra.Command{
Use: "analyze [dataset-path]",
Short: "Run vulnerability analysis on parsed data",
Long: `Analyze runs Claude-powered Stage 1 vulnerability detection on a parsed dataset.
Long: `Analyze runs LLM-powered Stage 1 vulnerability detection on a parsed dataset.

With --verify, it chains into Stage 2 attacker simulation automatically.
For standalone Stage 2, use the verify command instead.
Expand Down Expand Up @@ -124,7 +124,7 @@ func runAnalyze(cmd *cobra.Command, args []string) {
pyArgs = append(pyArgs, "--backoff", fmt.Sprintf("%d", analyzeBackoff))
}

result, err := python.Invoke(rt.Path, pyArgs, "", quiet, requireAPIKey())
result, err := python.Invoke(rt.Path, pyArgs, "", quiet, llmEnvRequired())
if err != nil {
output.PrintError(err.Error())
os.Exit(2)
Expand Down
2 changes: 1 addition & 1 deletion apps/openant-cli/cmd/buildoutput.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func runBuildOutput(cmd *cobra.Command, args []string) {
pyArgs = append(pyArgs, "--processing-level", buildOutputProcessingLevel)
}

result, err := python.Invoke(rt.Path, pyArgs, "", quiet, resolvedAPIKey())
result, err := python.Invoke(rt.Path, pyArgs, "", quiet, llmEnv())
if err != nil {
output.PrintError(err.Error())
os.Exit(2)
Expand Down
126 changes: 121 additions & 5 deletions apps/openant-cli/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ var configCmd = &cobra.Command{
Configuration is stored in ~/.config/openant/config.json.

Examples:
openant config set api-key Set your Anthropic API key (interactive)
openant config set api-key Set your API key (interactive)
openant config set base-url Set LLM endpoint for local AI
openant config set opus-model Set model for heavy analysis
openant config set sonnet-model Set model for lighter tasks
openant config show View current configuration
openant config unset api-key Remove your API key
openant config unset base-url Remove a setting
openant config path Print the config file path`,
}

Expand All @@ -31,10 +34,14 @@ var configSetCmd = &cobra.Command{
Long: `Set a configuration value. For sensitive values like api-key,
the value is read from stdin (not echoed) to avoid shell history exposure.

Supported keys: api-key, default-model
Supported keys: api-key, base-url, default-model, opus-model, sonnet-model, verify-ssl

Examples:
openant config set api-key Interactive prompt (recommended)
openant config set base-url Set LLM endpoint (e.g. http://localhost:8080)
openant config set opus-model Set model name for heavy analysis
openant config set sonnet-model Set model name for lighter tasks
openant config set verify-ssl Enable/disable SSL certificate verification
echo "sk-ant-..." | openant config set api-key --stdin Piped input`,
Args: cobra.ExactArgs(1),
Run: runConfigSet,
Expand Down Expand Up @@ -126,8 +133,97 @@ func runConfigSet(cmd *cobra.Command, args []string) {

cfg.DefaultModel = value

case "base-url":
if configStdin {
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
value = strings.TrimSpace(scanner.Text())
}
} else {
fmt.Fprint(os.Stderr, "Enter LLM API base URL (e.g. http://localhost:8080): ")
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
value = strings.TrimSpace(scanner.Text())
}
}

if value == "" {
output.PrintError("No value provided")
os.Exit(1)
}

cfg.BaseURL = value

case "opus-model":
if configStdin {
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
value = strings.TrimSpace(scanner.Text())
}
} else {
fmt.Fprint(os.Stderr, "Enter opus model name (heavy analysis, e.g. qwen3:32b): ")
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
value = strings.TrimSpace(scanner.Text())
}
}

if value == "" {
output.PrintError("No value provided")
os.Exit(1)
}

cfg.OpusModel = value

case "sonnet-model":
if configStdin {
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
value = strings.TrimSpace(scanner.Text())
}
} else {
fmt.Fprint(os.Stderr, "Enter sonnet model name (lighter tasks, e.g. qwen3:8b): ")
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
value = strings.TrimSpace(scanner.Text())
}
}

if value == "" {
output.PrintError("No value provided")
os.Exit(1)
}

cfg.SonnetModel = value

case "verify-ssl":
if configStdin {
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
value = strings.TrimSpace(scanner.Text())
}
} else {
fmt.Fprint(os.Stderr, "Verify SSL certificates? (true/false): ")
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
value = strings.TrimSpace(scanner.Text())
}
}

switch strings.ToLower(value) {
case "true", "yes", "1":
v := true
cfg.VerifySSL = &v
case "false", "no", "0":
v := false
cfg.VerifySSL = &v
default:
output.PrintError("Value must be true or false")
os.Exit(1)
}

default:
output.PrintError(fmt.Sprintf("Unknown config key: %s\nSupported keys: api-key, default-model", key))
output.PrintError(fmt.Sprintf("Unknown config key: %s\nSupported keys: api-key, base-url, default-model, opus-model, sonnet-model, verify-ssl", key))
os.Exit(1)
}

Expand All @@ -151,9 +247,21 @@ func runConfigShow(cmd *cobra.Command, args []string) {

output.PrintHeader("Configuration")
output.PrintKeyValue("api_key", config.MaskKey(cfg.APIKey))
if cfg.BaseURL != "" {
output.PrintKeyValue("base_url", cfg.BaseURL)
}
if cfg.DefaultModel != "" {
output.PrintKeyValue("default_model", cfg.DefaultModel)
}
if cfg.OpusModel != "" {
output.PrintKeyValue("opus_model", cfg.OpusModel)
}
if cfg.SonnetModel != "" {
output.PrintKeyValue("sonnet_model", cfg.SonnetModel)
}
if cfg.VerifySSL != nil {
output.PrintKeyValue("verify_ssl", fmt.Sprintf("%v", *cfg.VerifySSL))
}
if cfg.ActiveProject != "" {
output.PrintKeyValue("active_project", cfg.ActiveProject)
}
Expand All @@ -173,10 +281,18 @@ func runConfigUnset(cmd *cobra.Command, args []string) {
switch key {
case "api-key":
cfg.APIKey = ""
case "base-url":
cfg.BaseURL = ""
case "default-model":
cfg.DefaultModel = ""
case "opus-model":
cfg.OpusModel = ""
case "sonnet-model":
cfg.SonnetModel = ""
case "verify-ssl":
cfg.VerifySSL = nil
default:
output.PrintError(fmt.Sprintf("Unknown config key: %s\nSupported keys: api-key, default-model", key))
output.PrintError(fmt.Sprintf("Unknown config key: %s\nSupported keys: api-key, base-url, default-model, opus-model, sonnet-model, verify-ssl", key))
os.Exit(1)
}

Expand Down
2 changes: 1 addition & 1 deletion apps/openant-cli/cmd/dynamictest.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func runDynamicTest(cmd *cobra.Command, args []string) {
pyArgs = append(pyArgs, "--repo-path", ctx.RepoPath)
}

result, err := python.Invoke(rt.Path, pyArgs, "", quiet, requireAPIKey())
result, err := python.Invoke(rt.Path, pyArgs, "", quiet, llmEnvRequired())
if err != nil {
output.PrintError(err.Error())
os.Exit(2)
Expand Down
2 changes: 1 addition & 1 deletion apps/openant-cli/cmd/enhance.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func runEnhance(cmd *cobra.Command, args []string) {
pyArgs = append(pyArgs, "--backoff", fmt.Sprintf("%d", enhanceBackoff))
}

result, err := python.Invoke(rt.Path, pyArgs, "", quiet, requireAPIKey())
result, err := python.Invoke(rt.Path, pyArgs, "", quiet, llmEnvRequired())
if err != nil {
output.PrintError(err.Error())
os.Exit(2)
Expand Down
2 changes: 1 addition & 1 deletion apps/openant-cli/cmd/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func runParse(cmd *cobra.Command, args []string) {
pyArgs = append(pyArgs, "--diff-manifest", manifestPath)
}

result, err := python.Invoke(rt.Path, pyArgs, "", quiet, resolvedAPIKey())
result, err := python.Invoke(rt.Path, pyArgs, "", quiet, llmEnv())
if err != nil {
output.PrintError(err.Error())
os.Exit(2)
Expand Down
4 changes: 2 additions & 2 deletions apps/openant-cli/cmd/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ func runReport(cmd *cobra.Command, args []string) {
// Other formats delegate to Python
pyArgs := buildReportArgs(resultsPath, fmt)

result, err := python.Invoke(rt.Path, pyArgs, "", quiet, resolvedAPIKey())
result, err := python.Invoke(rt.Path, pyArgs, "", quiet, llmEnv())
if err != nil {
output.PrintError(fmt + ": " + err.Error())
exitCode = 2
Expand Down Expand Up @@ -312,7 +312,7 @@ func runHTMLReport(rt *python.RuntimeInfo, resultsPath string, outputPath string
pyArgs = append(pyArgs, "--dataset", reportDataset)
}

result, err := python.Invoke(rt.Path, pyArgs, "", quiet, resolvedAPIKey())
result, err := python.Invoke(rt.Path, pyArgs, "", quiet, llmEnv())
if err != nil {
return fmt.Errorf("report-data failed: %w", err)
}
Expand Down
53 changes: 48 additions & 5 deletions apps/openant-cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ var (
var rootCmd = &cobra.Command{
Use: "openant",
Short: "LLM-powered static analysis security testing",
Long: `OpenAnt is a two-stage SAST tool that uses Claude to find real vulnerabilities
Long: `OpenAnt is a two-stage SAST tool that uses LLMs to find real vulnerabilities
in Python, JavaScript, Go, and C/C++ codebases.

Works with Anthropic's API or any compatible local AI server (llama-swap,
llama-server, vLLM, LM Studio, etc.).

Stage 1: Detect potential vulnerabilities via code analysis
Stage 2: Simulate an attacker to eliminate false positives

Expand All @@ -40,7 +43,7 @@ Commands:
build-output Assemble pipeline_output.json from verified results
dynamic-test Docker-isolated exploit testing
report Generate reports from analysis results
config Manage CLI configuration (API key, etc.)`,
config Manage CLI configuration (API key, endpoint, models)`,
}

// Execute adds all child commands to the root command and sets flags appropriately.
Expand All @@ -65,17 +68,57 @@ func requireAPIKey() string {
}
fmt.Fprintln(os.Stderr, "Error: No API key configured.")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Run: openant set-api-key <your-anthropic-api-key>")
fmt.Fprintln(os.Stderr, "Run: openant config set api-key")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "You can get an API key at https://console.anthropic.com/settings/keys")
fmt.Fprintln(os.Stderr, "For local AI (llama-swap, llama-server, etc.), any value works:")
fmt.Fprintln(os.Stderr, " openant config set api-key (enter 'not-needed')")
fmt.Fprintln(os.Stderr, " openant config set base-url (enter your server URL)")
os.Exit(2)
return "" // unreachable
}

// llmEnv builds the environment variable map passed to the Python subprocess.
// It injects the API key, base URL, and model names from the config file
// so the Python core can connect to a local AI server or Anthropic's API.
func llmEnv() map[string]string {
env := map[string]string{}

key := resolvedAPIKey()
if key != "" {
env["ANTHROPIC_API_KEY"] = key
}

cfg, err := config.Load()
if err != nil {
return env
}

if cfg.BaseURL != "" {
env["ANTHROPIC_BASE_URL"] = cfg.BaseURL
}
if cfg.OpusModel != "" {
env["OPENANT_OPUS_MODEL"] = cfg.OpusModel
}
if cfg.SonnetModel != "" {
env["OPENANT_SONNET_MODEL"] = cfg.SonnetModel
}
if cfg.VerifySSL != nil && !*cfg.VerifySSL {
env["OPENANT_VERIFY_SSL"] = "false"
}

return env
}

// llmEnvRequired is like llmEnv but exits if no API key is configured.
func llmEnvRequired() map[string]string {
requireAPIKey() // exits if missing
return llmEnv()
}

func init() {
rootCmd.PersistentFlags().BoolVar(&jsonOutput, "json", false, "Output raw JSON (machine-readable)")
rootCmd.PersistentFlags().BoolVarP(&quiet, "quiet", "q", false, "Suppress progress output")
rootCmd.PersistentFlags().StringVar(&apiKeyFlag, "api-key", "", "Anthropic API key (overrides config)")
rootCmd.PersistentFlags().StringVar(&apiKeyFlag, "api-key", "", "LLM API key (overrides config)")
rootCmd.PersistentFlags().StringVarP(&projectFlag, "project", "p", "", "Project to use (overrides active project, e.g. grafana/grafana)")

rootCmd.AddCommand(initCmd)
Expand Down
2 changes: 1 addition & 1 deletion apps/openant-cli/cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ func runScan(cmd *cobra.Command, args []string) {
}
}

result, err := python.Invoke(rt.Path, pyArgs, "", quiet, requireAPIKey())
result, err := python.Invoke(rt.Path, pyArgs, "", quiet, llmEnvRequired())
if err != nil {
finalizeScanMetaIfProject(ctx, config.ScanStatusFailed)
output.PrintError(err.Error())
Expand Down
35 changes: 20 additions & 15 deletions apps/openant-cli/cmd/setapikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,19 @@ func validateAPIKey(key string) error {

var setAPIKeyCmd = &cobra.Command{
Use: "set-api-key <key>",
Short: "Save your Anthropic API key",
Long: `Save your Anthropic API key to the OpenAnt config file.
Short: "Save your LLM API key",
Long: `Save your LLM API key to the OpenAnt config file.

The key is stored in ~/.config/openant/config.json with restricted
permissions (0600). This is required before running enhance, analyze,
verify, or scan.

Get an API key at https://console.anthropic.com/settings/keys
For Anthropic: get a key at https://console.anthropic.com/settings/keys
For local AI (llama-swap, etc.): use any non-empty value (e.g. "not-needed")

Examples:
openant set-api-key sk-ant-api03-...`,
openant set-api-key sk-ant-api03-... Anthropic key
openant set-api-key not-needed Local AI (no real key needed)`,
Args: cobra.ExactArgs(1),
Run: runSetAPIKey,
}
Expand All @@ -62,23 +64,26 @@ func runSetAPIKey(cmd *cobra.Command, args []string) {
os.Exit(1)
}

// Validate against Anthropic BEFORE saving — a bad key should never
// be persisted, otherwise `openant scan` silently produces zero results
// that look like a clean repo.
fmt.Fprintf(os.Stderr, "Validating API key with Anthropic... ")
if err := validateAPIKey(key); err != nil {
fmt.Fprintf(os.Stderr, "\n")
output.PrintError(err.Error())
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "OK\n")

cfg, err := config.Load()
if err != nil {
output.PrintError(err.Error())
os.Exit(1)
}

// Only validate against Anthropic if no custom base URL is configured.
// With a local AI server the Anthropic validation endpoint is irrelevant.
if cfg.BaseURL == "" {
fmt.Fprintf(os.Stderr, "Validating API key with Anthropic... ")
if err := validateAPIKey(key); err != nil {
fmt.Fprintf(os.Stderr, "\n")
output.PrintError(err.Error())
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "OK\n")
} else {
fmt.Fprintf(os.Stderr, "Custom base URL configured — skipping Anthropic validation\n")
}

cfg.APIKey = key

if err := config.Save(cfg); err != nil {
Expand Down
Loading