mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
Add two optional free-text fields to findings so a reporter can record what was tested and did not work (counterevidence) and why the conclusion holds (confidence_rationale). - db: idempotent ADD COLUMN IF NOT EXISTS, TEXT NOT NULL DEFAULT '', no CHECK; RecordFindingInput, the INSERT, the shared select list and all three scan sites carry them. - agent: report_finding accepts both as optional strings; the worker and pentest prompts say to fill them when confidence is not high. - server: FindingDTO (list and detail) carries both, omitted when empty; existing databases get the two schema properties added to the stored report_finding row once, without overwriting user edits. - web: the finding detail page shows each section only when non-empty. Both fields stay optional: a missing field never refuses a finding.
71 lines
2.5 KiB
Go
71 lines
2.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Autumn-27/artex/db"
|
|
)
|
|
|
|
// Pure: report_finding advertises counterevidence / confidence_rationale as
|
|
// optional string properties.
|
|
func TestReportFindingSchemaReasoningFieldsOptional(t *testing.T) {
|
|
schema := NewToolSet(nil, "").addFinding().InputSchema()
|
|
props, _ := schema["properties"].(map[string]any)
|
|
required, _ := schema["required"].([]string)
|
|
if !slices.Contains(required, "vulnclass") {
|
|
t.Fatalf("unexpected required list shape: %#v", schema["required"])
|
|
}
|
|
for _, name := range []string{"counterevidence", "confidence_rationale"} {
|
|
p, ok := props[name].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("report_finding schema missing %s", name)
|
|
}
|
|
if p["type"] != "string" {
|
|
t.Fatalf("%s type = %v, want string", name, p["type"])
|
|
}
|
|
if desc, _ := p["description"].(string); !strings.HasPrefix(desc, "optional") {
|
|
t.Fatalf("%s description should say optional: %q", name, desc)
|
|
}
|
|
if slices.Contains(required, name) {
|
|
t.Fatalf("%s must not be required: %v", name, required)
|
|
}
|
|
}
|
|
}
|
|
|
|
// PG: the tool passes both fields into storage, and omitting them never blocks
|
|
// a finding.
|
|
func TestReportFindingStoresReasoningFields(t *testing.T) {
|
|
d := testDB(t)
|
|
defer d.Close()
|
|
task, err := d.CreateTask("reasoning fields", "fixture", nil, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer d.DeleteTask(task.ID)
|
|
ts := NewToolSet(nil, "")
|
|
ts.ts, ts.taskID = d.Exploration(task.ExplorationID), task.ID
|
|
for _, tc := range []struct{ extra, counter, rationale string }{
|
|
{`,"counterevidence":"WAF blocked 4 of 5 encodings","confidence_rationale":"the 5th returned the other tenant's row"`, "WAF blocked 4 of 5 encodings", "the 5th returned the other tenant's row"},
|
|
{"", "", ""},
|
|
} {
|
|
res, err := ts.addFinding().Call(t.Context(), json.RawMessage(`{"vulnclass":"SQLI","severity":"high","summary":"reasoning fixture","evidence":"output"`+tc.extra+`}`), nil)
|
|
if err != nil || !strings.HasPrefix(res.Flatten(), "finding recorded: ") {
|
|
t.Fatalf("report_finding rejected: %v %s", err, res.Flatten())
|
|
}
|
|
var out db.RecordedFinding
|
|
if err := json.Unmarshal([]byte(strings.SplitN(res.Flatten(), "\n", 2)[1]), &out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, err := d.GetFinding(out.FindingID)
|
|
if err != nil || f == nil {
|
|
t.Fatalf("GetFinding: %v %v", f, err)
|
|
}
|
|
if f.Counterevidence != tc.counter || f.ConfidenceRationale != tc.rationale {
|
|
t.Fatalf("stored (%q, %q), want (%q, %q)", f.Counterevidence, f.ConfidenceRationale, tc.counter, tc.rationale)
|
|
}
|
|
}
|
|
}
|