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
8 changes: 7 additions & 1 deletion pkg/output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,24 @@ func (tw *TableWriter) Write(results []types.Result) error {
}

table := tablewriter.NewWriter(tw.writer)
table.SetHeader([]string{"TARGET", "SERVICE", "SPECIFICITY", "CATEGORY", "MODELS", "ERROR"})
table.SetHeader([]string{"TARGET", "SERVICE", "SPECIFICITY", "CATEGORY", "AUTH", "MODELS", "ERROR"})
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)

for _, result := range results {
models := strings.Join(result.Models, ", ")

auth := "open"
if result.AuthRequired {
auth = "required"
}

table.Append([]string{
result.Target,
result.Service,
fmt.Sprintf("%d", result.Specificity),
result.Category,
auth,
models,
result.Error,
})
Expand Down
44 changes: 44 additions & 0 deletions pkg/output/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ func TestTableWriter_WriteSingleResult(t *testing.T) {
assert.Contains(t, output, "TARGET")
assert.Contains(t, output, "SERVICE")
assert.Contains(t, output, "SPECIFICITY")
assert.Contains(t, output, "AUTH")

assert.Contains(t, output, "https://api.openai.com")
assert.Contains(t, output, "OpenAI API")
assert.Contains(t, output, "75")
assert.Contains(t, output, "open")
}

func TestTableWriter_WriteMultipleResults(t *testing.T) {
Expand Down Expand Up @@ -315,6 +317,48 @@ func TestNewWriter_JSONL(t *testing.T) {
require.Len(t, lines, 1, "Should have 1 line")
}

func TestTableWriter_AuthRequired(t *testing.T) {
buf := &bytes.Buffer{}
writer := NewTableWriter(buf)

results := []types.Result{
{
Target: "https://api.example.com/v1/models",
Service: "openai-compatible",
Category: "generic",
Specificity: 1,
AuthRequired: true,
},
}

err := writer.Write(results)
require.NoError(t, err)

output := buf.String()
assert.Contains(t, output, "required")
}

func TestTableWriter_AuthOpen(t *testing.T) {
buf := &bytes.Buffer{}
writer := NewTableWriter(buf)

results := []types.Result{
{
Target: "https://api.example.com/v1/models",
Service: "ollama",
Category: "self-hosted",
Specificity: 100,
AuthRequired: false,
},
}

err := writer.Write(results)
require.NoError(t, err)

output := buf.String()
assert.Contains(t, output, "open")
}

func TestTableWriterModelsAndError(t *testing.T) {
tests := []struct {
name string
Expand Down
67 changes: 46 additions & 21 deletions pkg/scanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,18 @@ func (s *Scanner) Scan(target string, probes []*types.Probe, augustus bool) []ty
default:
}

matched, matchedReq := s.matchProbe(target, p)
matched, mr := s.matchProbe(target, p)
if !matched {
return nil
}

result := types.Result{
Target: target + matchedReq.Path,
Target: target + mr.Request.Path,
Service: p.Name,
MatchedRequest: matchedReq.Path,
MatchedRequest: mr.Request.Path,
Category: p.Category,
Specificity: p.GetSpecificity(),
AuthRequired: mr.StatusCode == http.StatusUnauthorized || mr.StatusCode == http.StatusForbidden,
}

if p.Models != nil {
Expand Down Expand Up @@ -117,63 +118,87 @@ func (s *Scanner) Scan(target string, probes []*types.Probe, augustus bool) []ty
return results
}

func (s *Scanner) matchProbe(target string, p *types.Probe) (bool, types.Request) {
type matchResult struct {
Request types.Request
StatusCode int
}

func (s *Scanner) matchProbe(target string, p *types.Probe) (bool, matchResult) {
if p.RequiresAll() {
return s.matchProbeAll(target, p)
}
return s.matchProbeAny(target, p)
}

func (s *Scanner) matchProbeAny(target string, p *types.Probe) (bool, types.Request) {
func (s *Scanner) matchProbeAny(target string, p *types.Probe) (bool, matchResult) {
for _, req := range p.Requests {
req.ApplyDefaults()

matched, err := s.DoRequest(target, req)
if err != nil || !matched {
statusCode, err := s.doRequestWithStatus(target, req)
if err != nil || statusCode == -1 {
continue
}

return true, req
return true, matchResult{Request: req, StatusCode: statusCode}
}

return false, types.Request{}
return false, matchResult{}
}

func (s *Scanner) matchProbeAll(target string, p *types.Probe) (bool, types.Request) {
func (s *Scanner) matchProbeAll(target string, p *types.Probe) (bool, matchResult) {
if len(p.Requests) == 0 {
return false, types.Request{}
return false, matchResult{}
}

var firstReq types.Request
var result matchResult
for i, req := range p.Requests {
req.ApplyDefaults()

matched, err := s.DoRequest(target, req)
if err != nil || !matched {
return false, types.Request{}
statusCode, err := s.doRequestWithStatus(target, req)
if err != nil || statusCode == -1 {
return false, matchResult{}
}

if i == 0 {
firstReq = req
result = matchResult{Request: req, StatusCode: statusCode}
}
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
result.StatusCode = statusCode
}
Comment on lines 162 to 167

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve auth from every require-all request

For require: all probes, this retains only the first response's status, so an authentication response from a later required request is discarded. This occurs in shipped probes: Flowise first matches / with 200 and then /api/v1/chatflows with 401 (probes/flowise.yaml:13-33), while Quivr first matches /openapi.json with 200 and then /brains/ with 403 (probes/quivr.yaml:15-38). Both therefore emit auth_required:false, allowing downstream dispatch to endpoints known to require credentials; aggregate the auth signal across all matched requests while preserving the first request for the existing target metadata.

Useful? React with 👍 / 👎.

}

return true, firstReq
return true, result
}

func (s *Scanner) DoRequest(target string, req types.Request) (bool, error) {
statusCode, err := s.doRequestWithStatus(target, req)
if err != nil {
return false, err
}
if statusCode == -1 {
return false, nil
Comment on lines 173 to +179

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return errors from DoRequest

When the HTTP request fails or GetRules rejects a malformed rule, doRequestWithStatus returns (-1, err), but this wrapper discards that error and reports (false, nil). This changes the exported method's prior contract and prevents direct callers from distinguishing an ordinary non-match from network failures or invalid probe rules; propagate the helper's error before interpreting the status code.

Useful? React with 👍 / 👎.

}
return true, nil
}

// doRequestWithStatus returns (statusCode, error) where statusCode == -1 means
// the request failed or rules didn't parse. Callers use the status code to
// derive auth signals (401/403 → auth required).
func (s *Scanner) doRequestWithStatus(target string, req types.Request) (int, error) {
resp, body, err := s.doHTTPRequest(target, req.Method, req.Path, req.Body, req.Headers)
if err != nil {
return false, fmt.Errorf("executing request: %w", err)
return -1, fmt.Errorf("executing request: %w", err)
}

rules, err := req.GetRules()
if err != nil {
return false, fmt.Errorf("parsing rules: %w", err)
return -1, fmt.Errorf("parsing rules: %w", err)
}

matched := probe.MatchRules(resp, body, rules)
return matched, nil
if probe.MatchRules(resp, body, rules) {
return resp.StatusCode, nil
}
return -1, nil
}

func (s *Scanner) fetchModels(target string, cfg *types.ModelsConfig) ([]string, error) {
Expand Down
Loading