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
41 changes: 40 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,46 @@ All notable changes to OpenAnt are documented in this file.

## [Unreleased]

This release syncs a large body of work from internal development. Highlights:
### Fixed

- **Disclosure code is now byte-faithful to source.** The disclosure
renderer pulls the actual file slice from the repo instead of rerunning
an LLM rewrite, so every finding's `Vulnerable Code` block matches the
real source.
- **No more silent 401s.** `openant set-api-key` validates the key on save
and fails loudly on bad input. `openant scan` prints a blocking warning
and exits non-zero when zero API calls succeed, so an all-401 run can no
longer masquerade as a clean repo.
- **CWE tagging is now systematic.** `pipeline_output.json` carries
non-null `cwe`, `cwe_id`, and `vulnerability_type` for every finding.
Stage 1 prompt asks for them directly rather than relying on the
renderer LLM to infer them from prose.
- **`[NOT PROVIDED]` placeholders eliminated.** Repo name, commit SHA, and
file count are threaded into every phase report envelope
(`parse.report.json`, `scan.report.json`) instead of being lost between
stages.
- **`Verified` column reflects the highest evidence tier.** `dynamic` >
`verified` > `static`, so dynamically reproduced findings show as
`dynamic` and the disclosure footer reads "Confirmed via dynamic test"
where applicable.
- **Call-graph-aware deduplication.** When two findings share a
sink/vector and the call graph records an edge between them, they
collapse into a single finding.
- **Dynamic test scaffolding fixed.** `openant dynamic-test` pre-stages
the vulnerable source file into the Docker build context end-to-end
through the dynamic-test chain — first-try Docker builds no longer fail
because the source isn't in context.
- **Concurrency-safe Docker resources.** Docker image and network names
get a UUID prefix so parallel dynamic-test workers can't collide.
- **Agreement filter checks the final verdict** instead of the
intermediate `agree` flag, so high-confidence dynamic results aren't
dropped by a stale agreement signal.
- **Dedup matches on CWE** instead of `attack_vector` text, so small
wording differences no longer split what's logically the same finding.

## [2026-04-14] — Initial public release

This release synced a large body of work from internal development. Highlights:

### Added

Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@

# OpenAnt

[OpenAnt](https://knostic.ai/openant) from [Knostic](https://knostic.ai) is an open source LLM-based vulnerability discovery product that helps defenders proactively find verified security flaws while minimizing both false positives and false negatives. Stage 1 detects. Stage 2 attacks. What survives is real.
[OpenAnt](https://knostic.ai/openant) from [Knostic](https://knostic.ai) is the leading open source LLM-based vulnerability discovery product, helping defenders proactively find verified security flaws while minimizing both false positives and false negatives. Stage 1 detects. Stage 2 attacks. What survives is real.

We're pretty proud of this product and are in the vulnerability disclosure process for its findings, but do keep in mind that this started as a research project, and some of its features are still in beta. We welcome contributions to make it better.

## Why open source?
Considering the explosion of AI-discovered vulnerabilities, we hope OpenAnt will be the tool helping open source maintainers stay ahead of attackers, where they can use it themselves or submit their repo for scanning at no cost.

Then, since Knostic's focus is on protecting agents and coding assistants and not vulnerability research or application security, and we like open source, we decided to release OpenAnt under the Apache 2 license.
Besides, you may have heard about Aardvark from OpenAI (now Codex Security) and Claude Code Security from Anthropic, and we have zero intention of competing with them.
While we do provide OpenAnt as a service, you may have heard about Aardvark from OpenAI (now Codex Security) and Claude Code Security from Anthropic, and we have zero intention of competing with them.

## Technical details and free scanning for open source projects
For technical details, limitations, and token costs, check out this blog post:
Expand Down
6 changes: 6 additions & 0 deletions apps/openant-cli/cmd/dynamictest.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ func runDynamicTest(cmd *cobra.Command, args []string) {
pyArgs = append(pyArgs, "--max-retries", fmt.Sprintf("%d", dynamicTestMaxRetries))
}

// Pass repo path so the dynamic tester can pre-stage source files into
// the Docker build context.
if ctx != nil && ctx.Project != nil && ctx.RepoPath != "" {
pyArgs = append(pyArgs, "--repo-path", ctx.RepoPath)
}

result, err := python.Invoke(rt.Path, pyArgs, "", quiet, requireAPIKey())
if err != nil {
output.PrintError(err.Error())
Expand Down
14 changes: 14 additions & 0 deletions apps/openant-cli/cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,20 @@ func runScan(cmd *cobra.Command, args []string) {
pyArgs = append(pyArgs, "--backoff", fmt.Sprintf("%d", scanBackoff))
}

// Pass repository metadata from project context so reports don't show
// [NOT PROVIDED] placeholders.
if ctx != nil && ctx.Project != nil {
if ctx.Project.Name != "" {
pyArgs = append(pyArgs, "--repo-name", ctx.Project.Name)
}
if ctx.Project.RepoURL != "" {
pyArgs = append(pyArgs, "--repo-url", ctx.Project.RepoURL)
}
if ctx.Project.CommitSHA != "" {
pyArgs = append(pyArgs, "--commit-sha", ctx.Project.CommitSHA)
}
}

result, err := python.Invoke(rt.Path, pyArgs, "", quiet, requireAPIKey())
if err != nil {
output.PrintError(err.Error())
Expand Down
39 changes: 39 additions & 0 deletions apps/openant-cli/cmd/setapikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,42 @@ package cmd

import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"

"github.com/knostic/open-ant-cli/internal/config"
"github.com/knostic/open-ant-cli/internal/output"
"github.com/spf13/cobra"
)

var anthropicAPIURL = "https://api.anthropic.com/v1/messages"

func validateAPIKey(key string) error {
body := strings.NewReader(`{"model":"claude-haiku-4-5-20251001","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}`)
req, err := http.NewRequest("POST", anthropicAPIURL, body)
if err != nil {
return fmt.Errorf("failed to build validation request: %w", err)
}
req.Header.Set("x-api-key", key)
req.Header.Set("anthropic-version", "2023-06-01")
req.Header.Set("content-type", "application/json")

client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("could not reach Anthropic API: %w", err)
}
defer func() { _, _ = io.Copy(io.Discard, resp.Body); resp.Body.Close() }()

if resp.StatusCode == http.StatusUnauthorized {
return fmt.Errorf("Anthropic rejected the key (HTTP 401). Double-check it at https://console.anthropic.com/settings/keys")
}
return nil
}

var setAPIKeyCmd = &cobra.Command{
Use: "set-api-key <key>",
Short: "Save your Anthropic API key",
Expand All @@ -34,6 +62,17 @@ 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())
Expand Down
85 changes: 85 additions & 0 deletions apps/openant-cli/cmd/setapikey_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package cmd

import (
"net/http"
"net/http/httptest"
"testing"
)

func TestValidateAPIKey_Rejects401(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
defer server.Close()

// Override the API URL for this test.
orig := anthropicAPIURL
defer func() { anthropicAPIURL = orig }()
anthropicAPIURL = server.URL

err := validateAPIKey("sk-bad-key")
if err == nil {
t.Fatal("expected error for 401 response, got nil")
}
if got := err.Error(); !contains(got, "401") {
t.Errorf("error should mention 401, got: %s", got)
}
}

func TestValidateAPIKey_AcceptsValid(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":"msg_test","type":"message","role":"assistant","content":[{"type":"text","text":"h"}],"model":"claude-haiku-4-5-20251001","usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()

orig := anthropicAPIURL
defer func() { anthropicAPIURL = orig }()
anthropicAPIURL = server.URL

if err := validateAPIKey("sk-good-key"); err != nil {
t.Fatalf("expected nil error for 200 response, got: %v", err)
}
}

func TestValidateAPIKey_SendsCorrectHeaders(t *testing.T) {
var gotKey, gotVersion, gotContentType string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotKey = r.Header.Get("x-api-key")
gotVersion = r.Header.Get("anthropic-version")
gotContentType = r.Header.Get("content-type")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{}`))
}))
defer server.Close()

orig := anthropicAPIURL
defer func() { anthropicAPIURL = orig }()
anthropicAPIURL = server.URL

_ = validateAPIKey("sk-test-123")

if gotKey != "sk-test-123" {
t.Errorf("x-api-key = %q, want %q", gotKey, "sk-test-123")
}
if gotVersion != "2023-06-01" {
t.Errorf("anthropic-version = %q, want %q", gotVersion, "2023-06-01")
}
if gotContentType != "application/json" {
t.Errorf("content-type = %q, want %q", gotContentType, "application/json")
}
}

func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
}

func containsHelper(s, sub string) bool {
for i := 0; i <= len(s)-len(sub); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}
2 changes: 2 additions & 0 deletions libs/openant-core/core/dynamic_tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def run_tests(
pipeline_output_path: str,
output_dir: str,
max_retries: int = 3,
repo_path: str | None = None,
) -> DynamicTestStepResult:
"""Run dynamic exploit tests on confirmed vulnerabilities.

Expand Down Expand Up @@ -81,6 +82,7 @@ def run_tests(
pipeline_output_path,
output_dir,
max_retries=max_retries,
repo_path=repo_path,
)

# Count outcomes
Expand Down
Loading
Loading