f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
418 lines
12 KiB
Go
418 lines
12 KiB
Go
//go:build pgtest
|
|
|
|
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"go.kenn.io/agentsview/internal/db"
|
|
)
|
|
|
|
// seedSecretFindingsSession inserts a session, optional messages, and findings.
|
|
func seedSecretFindingsSession(
|
|
t *testing.T, store *Store,
|
|
sid, project, agent string,
|
|
startedAt string,
|
|
msgs []struct {
|
|
ordinal int
|
|
role string
|
|
content string
|
|
},
|
|
findings []db.SecretFinding,
|
|
) {
|
|
t.Helper()
|
|
pg := store.DB()
|
|
|
|
_, err := pg.Exec(`DELETE FROM secret_findings WHERE session_id = $1`, sid)
|
|
require.NoError(t, err, "delete secret_findings")
|
|
_, err = pg.Exec(`DELETE FROM messages WHERE session_id = $1`, sid)
|
|
require.NoError(t, err, "delete messages")
|
|
_, err = pg.Exec(`DELETE FROM sessions WHERE id = $1`, sid)
|
|
require.NoError(t, err, "delete session")
|
|
|
|
var saVal interface{} = nil
|
|
if startedAt != "" {
|
|
saVal = startedAt
|
|
}
|
|
_, err = pg.Exec(`
|
|
INSERT INTO sessions
|
|
(id, machine, project, agent, first_message,
|
|
started_at, message_count, user_message_count,
|
|
secret_leak_count)
|
|
VALUES ($1, 'test-machine', $2, $3, 'test',
|
|
$4::timestamptz, $5, 0, $6)`,
|
|
sid, project, agent, saVal,
|
|
len(msgs)+1, len(findings),
|
|
)
|
|
require.NoError(t, err, "insert session %s", sid)
|
|
|
|
for _, m := range msgs {
|
|
_, err := pg.Exec(`
|
|
INSERT INTO messages
|
|
(session_id, ordinal, role, content, content_length)
|
|
VALUES ($1, $2, $3, $4, $5)`,
|
|
sid, m.ordinal, m.role, m.content, len(m.content),
|
|
)
|
|
require.NoError(t, err, "insert message ord=%d", m.ordinal)
|
|
}
|
|
|
|
for _, f := range findings {
|
|
_, err := pg.Exec(`
|
|
INSERT INTO secret_findings
|
|
(session_id, rule_name, confidence, location_kind,
|
|
message_ordinal, call_index, event_index,
|
|
match_start, match_end, match_index,
|
|
redacted_match, rules_version)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)`,
|
|
f.SessionID, f.RuleName, f.Confidence, f.LocationKind,
|
|
f.MessageOrdinal, f.CallIndex, f.EventIndex,
|
|
f.MatchStart, f.MatchEnd, f.MatchIndex,
|
|
f.RedactedMatch, f.RulesVersion,
|
|
)
|
|
require.NoError(t, err, "insert finding")
|
|
}
|
|
}
|
|
|
|
func TestPGListSecretFindings(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
ensureStoreSchema(t, pgURL)
|
|
|
|
store, err := NewStore(pgURL, testSchema, true)
|
|
require.NoError(t, err, "NewStore")
|
|
defer store.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
seedSecretFindingsSession(t, store,
|
|
"sf-s1", "alpha-proj", "claude-code",
|
|
"2026-03-12T10:00:00Z",
|
|
nil,
|
|
[]db.SecretFinding{
|
|
{SessionID: "sf-s1", RuleName: "aws-access-key",
|
|
Confidence: "definite", LocationKind: "message",
|
|
MessageOrdinal: 0, MatchStart: 0, MatchEnd: 20,
|
|
MatchIndex: 0, RedactedMatch: "AKIA…", RulesVersion: "v1"},
|
|
},
|
|
)
|
|
seedSecretFindingsSession(t, store,
|
|
"sf-s2", "beta-proj", "codex",
|
|
"2026-03-13T10:00:00Z",
|
|
nil,
|
|
[]db.SecretFinding{
|
|
{SessionID: "sf-s2", RuleName: "jwt",
|
|
Confidence: "candidate", LocationKind: "message",
|
|
MessageOrdinal: 0, MatchStart: 0, MatchEnd: 10,
|
|
MatchIndex: 0, RedactedMatch: "eyJ…", RulesVersion: "v1"},
|
|
},
|
|
)
|
|
|
|
// All findings.
|
|
all, err := store.ListSecretFindings(ctx, db.SecretFindingFilter{Limit: 50})
|
|
require.NoError(t, err, "ListSecretFindings all")
|
|
// At least 2 findings (may be more from other tests).
|
|
foundS1, foundS2 := false, false
|
|
for _, f := range all.Findings {
|
|
if f.SessionID == "sf-s1" {
|
|
foundS1 = true
|
|
assert.Equal(t, "alpha-proj", f.Project, "sf-s1 Project")
|
|
assert.Equal(t, "claude-code", f.Agent, "sf-s1 Agent")
|
|
}
|
|
if f.SessionID == "sf-s2" {
|
|
foundS2 = true
|
|
assert.Equal(t, "beta-proj", f.Project, "sf-s2 Project")
|
|
assert.Equal(t, "codex", f.Agent, "sf-s2 Agent")
|
|
}
|
|
}
|
|
assert.True(t, foundS1 && foundS2,
|
|
"foundS1=%v foundS2=%v; both must be present", foundS1, foundS2)
|
|
|
|
// Project filter.
|
|
alpha, err := store.ListSecretFindings(ctx,
|
|
db.SecretFindingFilter{Project: "alpha-proj", Limit: 50})
|
|
require.NoError(t, err, "ListSecretFindings project filter")
|
|
for _, f := range alpha.Findings {
|
|
assert.Equal(t, "alpha-proj", f.Project, "project filter leak")
|
|
}
|
|
found := false
|
|
for _, f := range alpha.Findings {
|
|
if f.SessionID == "sf-s1" {
|
|
found = true
|
|
}
|
|
}
|
|
assert.True(t, found, "sf-s1 not found in project filter results")
|
|
|
|
// Agent filter.
|
|
codex, err := store.ListSecretFindings(ctx,
|
|
db.SecretFindingFilter{Agent: "codex", Limit: 50})
|
|
require.NoError(t, err, "ListSecretFindings agent filter")
|
|
for _, f := range codex.Findings {
|
|
assert.Equal(t, "codex", f.Agent, "agent filter leak")
|
|
}
|
|
|
|
// Confidence filter: definite.
|
|
def, err := store.ListSecretFindings(ctx,
|
|
db.SecretFindingFilter{Confidence: "definite", Limit: 50})
|
|
require.NoError(t, err, "ListSecretFindings confidence filter")
|
|
for _, f := range def.Findings {
|
|
assert.Equal(t, "definite", f.Confidence, "confidence filter leak")
|
|
}
|
|
|
|
// Rules-version filter.
|
|
current, err := store.ListSecretFindings(ctx,
|
|
db.SecretFindingFilter{RulesVersions: []string{"v1"}, Limit: 50})
|
|
require.NoError(t, err, "ListSecretFindings rules version filter")
|
|
foundS1, foundS2 = false, false
|
|
for _, f := range current.Findings {
|
|
assert.Equal(t, "v1", f.RulesVersion, "rules version filter leak")
|
|
if f.SessionID == "sf-s1" {
|
|
foundS1 = true
|
|
}
|
|
if f.SessionID == "sf-s2" {
|
|
foundS2 = true
|
|
}
|
|
}
|
|
assert.True(t, foundS1 && foundS2,
|
|
"rules version filter foundS1=%v foundS2=%v", foundS1, foundS2)
|
|
stale, err := store.ListSecretFindings(ctx,
|
|
db.SecretFindingFilter{RulesVersions: []string{"v2"}, Limit: 50})
|
|
require.NoError(t, err, "ListSecretFindings stale rules version filter")
|
|
for _, f := range stale.Findings {
|
|
assert.NotContains(t, []string{"sf-s1", "sf-s2"}, f.SessionID,
|
|
"stale rules version filter returned %s", f.SessionID)
|
|
}
|
|
|
|
// Rule filter.
|
|
jwt, err := store.ListSecretFindings(ctx,
|
|
db.SecretFindingFilter{Rule: "jwt", Limit: 50})
|
|
require.NoError(t, err, "ListSecretFindings rule filter")
|
|
for _, f := range jwt.Findings {
|
|
assert.Equal(t, "jwt", f.RuleName, "rule filter leak")
|
|
}
|
|
}
|
|
|
|
func TestPGListSecretFindingsPagination(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
ensureStoreSchema(t, pgURL)
|
|
|
|
store, err := NewStore(pgURL, testSchema, true)
|
|
require.NoError(t, err, "NewStore")
|
|
defer store.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Seed 5 findings on a single session with a distinctive rule name.
|
|
findings := make([]db.SecretFinding, 5)
|
|
for i := range findings {
|
|
findings[i] = db.SecretFinding{
|
|
SessionID: "sf-page-s1", RuleName: "pg-pagination-test",
|
|
Confidence: "definite", LocationKind: "message",
|
|
MessageOrdinal: 0, MatchStart: i * 10, MatchEnd: i*10 + 5,
|
|
MatchIndex: i, RedactedMatch: "X…", RulesVersion: "v1",
|
|
}
|
|
}
|
|
seedSecretFindingsSession(t, store,
|
|
"sf-page-s1", "page-proj", "claude-code",
|
|
"2026-04-01T10:00:00Z",
|
|
nil, findings,
|
|
)
|
|
|
|
seen := map[int]int{}
|
|
cursor, pages := 0, 0
|
|
for {
|
|
page, err := store.ListSecretFindings(ctx,
|
|
db.SecretFindingFilter{
|
|
Rule: "pg-pagination-test",
|
|
Limit: 2, Cursor: cursor,
|
|
})
|
|
require.NoError(t, err, "page at cursor %d", cursor)
|
|
for _, f := range page.Findings {
|
|
seen[f.MatchStart]++
|
|
}
|
|
pages++
|
|
require.LessOrEqual(t, pages, 10, "pagination did not terminate")
|
|
if page.NextCursor == 0 {
|
|
break
|
|
}
|
|
cursor = page.NextCursor
|
|
}
|
|
require.Len(t, seen, 5, "distinct findings across pages")
|
|
for start, n := range seen {
|
|
assert.Equal(t, 1, n,
|
|
"finding at MatchStart=%d seen %d times", start, n)
|
|
}
|
|
}
|
|
|
|
func TestPGListSecretFindingsDateFilter(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
ensureStoreSchema(t, pgURL)
|
|
|
|
store, err := NewStore(pgURL, testSchema, true)
|
|
require.NoError(t, err, "NewStore")
|
|
defer store.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
seedSecretFindingsSession(t, store,
|
|
"sf-date-mar", "df-proj", "claude-code",
|
|
"2026-03-15T10:00:00Z",
|
|
nil,
|
|
[]db.SecretFinding{{
|
|
SessionID: "sf-date-mar", RuleName: "df-rule",
|
|
Confidence: "definite", LocationKind: "message",
|
|
MessageOrdinal: 0, MatchStart: 0, MatchEnd: 5,
|
|
MatchIndex: 0, RedactedMatch: "X", RulesVersion: "v1",
|
|
}},
|
|
)
|
|
seedSecretFindingsSession(t, store,
|
|
"sf-date-may", "df-proj", "claude-code",
|
|
"2026-05-10T10:00:00Z",
|
|
nil,
|
|
[]db.SecretFinding{{
|
|
SessionID: "sf-date-may", RuleName: "df-rule",
|
|
Confidence: "definite", LocationKind: "message",
|
|
MessageOrdinal: 0, MatchStart: 0, MatchEnd: 5,
|
|
MatchIndex: 0, RedactedMatch: "Y", RulesVersion: "v1",
|
|
}},
|
|
)
|
|
|
|
// Wide range includes both.
|
|
wide, err := store.ListSecretFindings(ctx,
|
|
db.SecretFindingFilter{
|
|
Rule: "df-rule", DateFrom: "2000-01-01", Limit: 50,
|
|
})
|
|
require.NoError(t, err, "wide")
|
|
require.Len(t, wide.Findings, 2)
|
|
|
|
// March-only range.
|
|
mar, err := store.ListSecretFindings(ctx,
|
|
db.SecretFindingFilter{
|
|
Rule: "df-rule",
|
|
DateFrom: "2026-03-01", DateTo: "2026-03-31",
|
|
Limit: 50,
|
|
})
|
|
require.NoError(t, err, "march")
|
|
require.Len(t, mar.Findings, 1)
|
|
assert.Equal(t, "sf-date-mar", mar.Findings[0].SessionID)
|
|
}
|
|
|
|
func TestPGSecretFindingSource(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
ensureStoreSchema(t, pgURL)
|
|
|
|
store, err := NewStore(pgURL, testSchema, true)
|
|
require.NoError(t, err, "NewStore")
|
|
defer store.Close()
|
|
|
|
pg := store.DB()
|
|
ctx := context.Background()
|
|
|
|
// Clean up prior runs. The sessions table uses "id" not "session_id".
|
|
sid := "sf-src-s1"
|
|
for _, tbl := range []string{
|
|
"secret_findings", "tool_result_events",
|
|
"tool_calls", "messages",
|
|
} {
|
|
_, err := pg.Exec("DELETE FROM "+tbl+" WHERE session_id = $1", sid)
|
|
require.NoError(t, err, "cleanup %s", tbl)
|
|
}
|
|
_, err = pg.Exec("DELETE FROM sessions WHERE id = $1", sid)
|
|
require.NoError(t, err, "cleanup sessions")
|
|
|
|
_, err = pg.Exec(`
|
|
INSERT INTO sessions
|
|
(id, machine, project, agent, first_message,
|
|
started_at, message_count, user_message_count)
|
|
VALUES ($1, 'test-machine', 'src-proj', 'claude-code',
|
|
'test', '2026-05-01T00:00:00Z'::timestamptz, 1, 0)`,
|
|
sid,
|
|
)
|
|
require.NoError(t, err, "insert session")
|
|
_, err = pg.Exec(`
|
|
INSERT INTO messages
|
|
(session_id, ordinal, role, content, content_length)
|
|
VALUES ($1, 0, 'assistant',
|
|
'key AKIA7QHWN2DKR4FYPLJM here', 28)`,
|
|
sid,
|
|
)
|
|
require.NoError(t, err, "insert message")
|
|
// Insert a tool_call at call_index=0 with result_content.
|
|
_, err = pg.Exec(`
|
|
INSERT INTO tool_calls
|
|
(session_id, message_ordinal, call_index, tool_name, category,
|
|
tool_use_id, input_json, result_content)
|
|
VALUES ($1, 0, 0, 'Bash', 'Bash', 'tu0',
|
|
'{"command":"printenv"}', 'AWS_SECRET=topsecretvalue123')`,
|
|
sid,
|
|
)
|
|
require.NoError(t, err, "insert tool_call")
|
|
// Insert a tool_call at call_index=1 with a result event.
|
|
_, err = pg.Exec(`
|
|
INSERT INTO tool_calls
|
|
(session_id, message_ordinal, call_index, tool_name, category,
|
|
tool_use_id)
|
|
VALUES ($1, 0, 1, 'Bash', 'Bash', 'tu1')`,
|
|
sid,
|
|
)
|
|
require.NoError(t, err, "insert tool_call 2")
|
|
_, err = pg.Exec(`
|
|
INSERT INTO tool_result_events
|
|
(session_id, tool_call_message_ordinal, call_index,
|
|
tool_use_id, source, status, content, event_index)
|
|
VALUES ($1, 0, 1, 'tu1', 'stdout', 'ok',
|
|
'event-secret-value', 0)`,
|
|
sid,
|
|
)
|
|
require.NoError(t, err, "insert tool_result_event")
|
|
|
|
cases := []struct {
|
|
name string
|
|
f db.SecretFinding
|
|
want string
|
|
ok bool
|
|
}{
|
|
{"message",
|
|
db.SecretFinding{SessionID: sid, LocationKind: "message",
|
|
MessageOrdinal: 0},
|
|
"key AKIA7QHWN2DKR4FYPLJM here", true},
|
|
{"tool_input",
|
|
db.SecretFinding{SessionID: sid, LocationKind: "tool_input",
|
|
MessageOrdinal: 0, CallIndex: ptr(0)},
|
|
`{"command":"printenv"}`, true},
|
|
{"tool_result",
|
|
db.SecretFinding{SessionID: sid, LocationKind: "tool_result",
|
|
MessageOrdinal: 0, CallIndex: ptr(0)},
|
|
"AWS_SECRET=topsecretvalue123", true},
|
|
{"tool_result_event",
|
|
db.SecretFinding{SessionID: sid, LocationKind: "tool_result_event",
|
|
MessageOrdinal: 0, CallIndex: ptr(1), EventIndex: ptr(0)},
|
|
"event-secret-value", true},
|
|
{"missing ordinal",
|
|
db.SecretFinding{SessionID: sid, LocationKind: "message",
|
|
MessageOrdinal: 99},
|
|
"", false},
|
|
{"call index out of range",
|
|
db.SecretFinding{SessionID: sid, LocationKind: "tool_input",
|
|
MessageOrdinal: 0, CallIndex: ptr(9)},
|
|
"", false},
|
|
{"tool_result skipped when events present",
|
|
db.SecretFinding{SessionID: sid, LocationKind: "tool_result",
|
|
MessageOrdinal: 0, CallIndex: ptr(1)},
|
|
"", false},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, ok, err := store.SecretFindingSource(ctx, tc.f)
|
|
require.NoError(t, err, "SecretFindingSource")
|
|
assert.Equal(t, tc.ok, ok)
|
|
assert.Equal(t, tc.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func ptr(i int) *int { return &i }
|