feat: add session indicators and top exec commands to dashboard
Add visual indicators to session tables (replay badge when events exist, exec badge for exec sessions) and a new "Top Exec Commands" table on the dashboard. Includes EventCount field on Session, GetTopExecCommands on Store interface, and truncateCommand template function. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -336,12 +336,20 @@ func (m *MemoryStore) GetRecentSessions(_ context.Context, limit int, activeOnly
|
|||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
// Count events per session.
|
||||||
|
eventCounts := make(map[string]int)
|
||||||
|
for _, e := range m.SessionEvents {
|
||||||
|
eventCounts[e.SessionID]++
|
||||||
|
}
|
||||||
|
|
||||||
var sessions []Session
|
var sessions []Session
|
||||||
for _, s := range m.Sessions {
|
for _, s := range m.Sessions {
|
||||||
if activeOnly && s.DisconnectedAt != nil {
|
if activeOnly && s.DisconnectedAt != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sessions = append(sessions, *s)
|
sess := *s
|
||||||
|
sess.EventCount = eventCounts[s.ID]
|
||||||
|
sessions = append(sessions, sess)
|
||||||
}
|
}
|
||||||
sort.Slice(sessions, func(i, j int) bool {
|
sort.Slice(sessions, func(i, j int) bool {
|
||||||
return sessions[i].ConnectedAt.After(sessions[j].ConnectedAt)
|
return sessions[i].ConnectedAt.After(sessions[j].ConnectedAt)
|
||||||
@@ -352,6 +360,30 @@ func (m *MemoryStore) GetRecentSessions(_ context.Context, limit int, activeOnly
|
|||||||
return sessions, nil
|
return sessions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MemoryStore) GetTopExecCommands(_ context.Context, limit int) ([]TopEntry, error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
counts := make(map[string]int64)
|
||||||
|
for _, s := range m.Sessions {
|
||||||
|
if s.ExecCommand != nil {
|
||||||
|
counts[*s.ExecCommand]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := make([]TopEntry, 0, len(counts))
|
||||||
|
for k, v := range counts {
|
||||||
|
entries = append(entries, TopEntry{Value: k, Count: v})
|
||||||
|
}
|
||||||
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
|
return entries[i].Count > entries[j].Count
|
||||||
|
})
|
||||||
|
if limit > 0 && len(entries) > limit {
|
||||||
|
entries = entries[:limit]
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MemoryStore) CloseActiveSessions(_ context.Context, disconnectedAt time.Time) (int64, error) {
|
func (m *MemoryStore) CloseActiveSessions(_ context.Context, disconnectedAt time.Time) (int64, error) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|||||||
@@ -382,11 +382,11 @@ func (s *SQLiteStore) queryTopN(ctx context.Context, column string, limit int) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQLiteStore) GetRecentSessions(ctx context.Context, limit int, activeOnly bool) ([]Session, error) {
|
func (s *SQLiteStore) GetRecentSessions(ctx context.Context, limit int, activeOnly bool) ([]Session, error) {
|
||||||
query := `SELECT id, ip, country, username, shell_name, connected_at, disconnected_at, human_score, exec_command FROM sessions`
|
query := `SELECT s.id, s.ip, s.country, s.username, s.shell_name, s.connected_at, s.disconnected_at, s.human_score, s.exec_command, COUNT(e.id) as event_count FROM sessions s LEFT JOIN session_events e ON s.id = e.session_id`
|
||||||
if activeOnly {
|
if activeOnly {
|
||||||
query += ` WHERE disconnected_at IS NULL`
|
query += ` WHERE s.disconnected_at IS NULL`
|
||||||
}
|
}
|
||||||
query += ` ORDER BY connected_at DESC LIMIT ?`
|
query += ` GROUP BY s.id ORDER BY s.connected_at DESC LIMIT ?`
|
||||||
|
|
||||||
rows, err := s.db.QueryContext(ctx, query, limit)
|
rows, err := s.db.QueryContext(ctx, query, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -401,7 +401,7 @@ func (s *SQLiteStore) GetRecentSessions(ctx context.Context, limit int, activeOn
|
|||||||
var disconnectedAt sql.NullString
|
var disconnectedAt sql.NullString
|
||||||
var humanScore sql.NullFloat64
|
var humanScore sql.NullFloat64
|
||||||
var execCommand sql.NullString
|
var execCommand sql.NullString
|
||||||
if err := rows.Scan(&s.ID, &s.IP, &s.Country, &s.Username, &s.ShellName, &connectedAt, &disconnectedAt, &humanScore, &execCommand); err != nil {
|
if err := rows.Scan(&s.ID, &s.IP, &s.Country, &s.Username, &s.ShellName, &connectedAt, &disconnectedAt, &humanScore, &execCommand, &s.EventCount); err != nil {
|
||||||
return nil, fmt.Errorf("scanning session: %w", err)
|
return nil, fmt.Errorf("scanning session: %w", err)
|
||||||
}
|
}
|
||||||
s.ConnectedAt, _ = time.Parse(time.RFC3339, connectedAt)
|
s.ConnectedAt, _ = time.Parse(time.RFC3339, connectedAt)
|
||||||
@@ -420,6 +420,30 @@ func (s *SQLiteStore) GetRecentSessions(ctx context.Context, limit int, activeOn
|
|||||||
return sessions, rows.Err()
|
return sessions, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SQLiteStore) GetTopExecCommands(ctx context.Context, limit int) ([]TopEntry, error) {
|
||||||
|
rows, err := s.db.QueryContext(ctx, `
|
||||||
|
SELECT exec_command, COUNT(*) as total
|
||||||
|
FROM sessions
|
||||||
|
WHERE exec_command IS NOT NULL
|
||||||
|
GROUP BY exec_command
|
||||||
|
ORDER BY total DESC
|
||||||
|
LIMIT ?`, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("querying top exec commands: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
var entries []TopEntry
|
||||||
|
for rows.Next() {
|
||||||
|
var e TopEntry
|
||||||
|
if err := rows.Scan(&e.Value, &e.Count); err != nil {
|
||||||
|
return nil, fmt.Errorf("scanning top exec commands: %w", err)
|
||||||
|
}
|
||||||
|
entries = append(entries, e)
|
||||||
|
}
|
||||||
|
return entries, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SQLiteStore) CloseActiveSessions(ctx context.Context, disconnectedAt time.Time) (int64, error) {
|
func (s *SQLiteStore) CloseActiveSessions(ctx context.Context, disconnectedAt time.Time) (int64, error) {
|
||||||
res, err := s.db.ExecContext(ctx, `
|
res, err := s.db.ExecContext(ctx, `
|
||||||
UPDATE sessions SET disconnected_at = ? WHERE disconnected_at IS NULL`,
|
UPDATE sessions SET disconnected_at = ? WHERE disconnected_at IS NULL`,
|
||||||
|
|||||||
@@ -204,6 +204,79 @@ func TestDeleteRecordsBefore(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetTopExecCommands(t *testing.T) {
|
||||||
|
store := newTestStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create sessions with exec commands.
|
||||||
|
for range 3 {
|
||||||
|
id, err := store.CreateSession(ctx, "10.0.0.1", "root", "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating session: %v", err)
|
||||||
|
}
|
||||||
|
if err := store.SetExecCommand(ctx, id, "uname -a"); err != nil {
|
||||||
|
t.Fatalf("setting exec command: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for range 2 {
|
||||||
|
id, err := store.CreateSession(ctx, "10.0.0.2", "admin", "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating session: %v", err)
|
||||||
|
}
|
||||||
|
if err := store.SetExecCommand(ctx, id, "cat /etc/passwd"); err != nil {
|
||||||
|
t.Fatalf("setting exec command: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Session without exec command — should not appear.
|
||||||
|
if _, err := store.CreateSession(ctx, "10.0.0.3", "test", "bash", ""); err != nil {
|
||||||
|
t.Fatalf("creating session: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := store.GetTopExecCommands(ctx, 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetTopExecCommands: %v", err)
|
||||||
|
}
|
||||||
|
if len(entries) != 2 {
|
||||||
|
t.Fatalf("len = %d, want 2", len(entries))
|
||||||
|
}
|
||||||
|
if entries[0].Value != "uname -a" || entries[0].Count != 3 {
|
||||||
|
t.Errorf("entries[0] = %+v, want uname -a:3", entries[0])
|
||||||
|
}
|
||||||
|
if entries[1].Value != "cat /etc/passwd" || entries[1].Count != 2 {
|
||||||
|
t.Errorf("entries[1] = %+v, want cat /etc/passwd:2", entries[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRecentSessionsEventCount(t *testing.T) {
|
||||||
|
store := newTestStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
id, err := store.CreateSession(ctx, "10.0.0.1", "root", "bash", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating session: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some events.
|
||||||
|
events := []SessionEvent{
|
||||||
|
{SessionID: id, Timestamp: time.Now(), Direction: 0, Data: []byte("ls\n")},
|
||||||
|
{SessionID: id, Timestamp: time.Now(), Direction: 1, Data: []byte("file1\n")},
|
||||||
|
}
|
||||||
|
if err := store.AppendSessionEvents(ctx, events); err != nil {
|
||||||
|
t.Fatalf("appending events: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions, err := store.GetRecentSessions(ctx, 10, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetRecentSessions: %v", err)
|
||||||
|
}
|
||||||
|
if len(sessions) != 1 {
|
||||||
|
t.Fatalf("len = %d, want 1", len(sessions))
|
||||||
|
}
|
||||||
|
if sessions[0].EventCount != 2 {
|
||||||
|
t.Errorf("EventCount = %d, want 2", sessions[0].EventCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewSQLiteStoreCreatesFile(t *testing.T) {
|
func TestNewSQLiteStoreCreatesFile(t *testing.T) {
|
||||||
dbPath := filepath.Join(t.TempDir(), "test.db")
|
dbPath := filepath.Join(t.TempDir(), "test.db")
|
||||||
store, err := NewSQLiteStore(dbPath)
|
store, err := NewSQLiteStore(dbPath)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type Session struct {
|
|||||||
DisconnectedAt *time.Time
|
DisconnectedAt *time.Time
|
||||||
HumanScore *float64
|
HumanScore *float64
|
||||||
ExecCommand *string
|
ExecCommand *string
|
||||||
|
EventCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionLog represents a single log entry for a session.
|
// SessionLog represents a single log entry for a session.
|
||||||
@@ -102,6 +103,9 @@ type Store interface {
|
|||||||
// GetTopCountries returns the top N countries by total attempt count.
|
// GetTopCountries returns the top N countries by total attempt count.
|
||||||
GetTopCountries(ctx context.Context, limit int) ([]TopEntry, error)
|
GetTopCountries(ctx context.Context, limit int) ([]TopEntry, error)
|
||||||
|
|
||||||
|
// GetTopExecCommands returns the top N exec commands by session count.
|
||||||
|
GetTopExecCommands(ctx context.Context, limit int) ([]TopEntry, error)
|
||||||
|
|
||||||
// GetRecentSessions returns the most recent sessions ordered by connected_at DESC.
|
// GetRecentSessions returns the most recent sessions ordered by connected_at DESC.
|
||||||
// If activeOnly is true, only sessions with no disconnected_at are returned.
|
// If activeOnly is true, only sessions with no disconnected_at are returned.
|
||||||
GetRecentSessions(ctx context.Context, limit int, activeOnly bool) ([]Session, error)
|
GetRecentSessions(ctx context.Context, limit int, activeOnly bool) ([]Session, error)
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type dashboardData struct {
|
type dashboardData struct {
|
||||||
Stats *storage.DashboardStats
|
Stats *storage.DashboardStats
|
||||||
TopUsernames []storage.TopEntry
|
TopUsernames []storage.TopEntry
|
||||||
TopPasswords []storage.TopEntry
|
TopPasswords []storage.TopEntry
|
||||||
TopIPs []storage.TopEntry
|
TopIPs []storage.TopEntry
|
||||||
TopCountries []storage.TopEntry
|
TopCountries []storage.TopEntry
|
||||||
ActiveSessions []storage.Session
|
TopExecCommands []storage.TopEntry
|
||||||
RecentSessions []storage.Session
|
ActiveSessions []storage.Session
|
||||||
|
RecentSessions []storage.Session
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -56,6 +57,13 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
topExecCommands, err := s.store.GetTopExecCommands(ctx, 10)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("failed to get top exec commands", "err", err)
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
activeSessions, err := s.store.GetRecentSessions(ctx, 50, true)
|
activeSessions, err := s.store.GetRecentSessions(ctx, 50, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("failed to get active sessions", "err", err)
|
s.logger.Error("failed to get active sessions", "err", err)
|
||||||
@@ -71,13 +79,14 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data := dashboardData{
|
data := dashboardData{
|
||||||
Stats: stats,
|
Stats: stats,
|
||||||
TopUsernames: topUsernames,
|
TopUsernames: topUsernames,
|
||||||
TopPasswords: topPasswords,
|
TopPasswords: topPasswords,
|
||||||
TopIPs: topIPs,
|
TopIPs: topIPs,
|
||||||
TopCountries: topCountries,
|
TopCountries: topCountries,
|
||||||
ActiveSessions: activeSessions,
|
TopExecCommands: topExecCommands,
|
||||||
RecentSessions: recentSessions,
|
ActiveSessions: activeSessions,
|
||||||
|
RecentSessions: recentSessions,
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ func templateFuncMap() template.FuncMap {
|
|||||||
}
|
}
|
||||||
return *s
|
return *s
|
||||||
},
|
},
|
||||||
|
"truncateCommand": func(s string) string {
|
||||||
|
if len(s) > 50 {
|
||||||
|
return s[:50] + "..."
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,21 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</article>
|
</article>
|
||||||
|
<article>
|
||||||
|
<header>Top Exec Commands</header>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Command</th><th>Count</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .TopExecCommands}}
|
||||||
|
<tr><td><code>{{truncateCommand .Value}}</code></td><td>{{.Count}}</td></tr>
|
||||||
|
{{else}}
|
||||||
|
<tr><td colspan="2">No data</td></tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -85,7 +100,7 @@
|
|||||||
<th>IP</th>
|
<th>IP</th>
|
||||||
<th>Country</th>
|
<th>Country</th>
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
<th>Shell</th>
|
<th>Type</th>
|
||||||
<th>Score</th>
|
<th>Score</th>
|
||||||
<th>Connected</th>
|
<th>Connected</th>
|
||||||
<th>Disconnected</th>
|
<th>Disconnected</th>
|
||||||
@@ -94,11 +109,11 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{{range .RecentSessions}}
|
{{range .RecentSessions}}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="/sessions/{{.ID}}"><code>{{truncateID .ID}}</code></a></td>
|
<td><a href="/sessions/{{.ID}}"><code>{{truncateID .ID}}</code></a>{{if gt .EventCount 0}} <mark>replay</mark>{{end}}</td>
|
||||||
<td>{{.IP}}</td>
|
<td>{{.IP}}</td>
|
||||||
<td>{{.Country}}</td>
|
<td>{{.Country}}</td>
|
||||||
<td>{{.Username}}</td>
|
<td>{{.Username}}</td>
|
||||||
<td>{{.ShellName}}</td>
|
<td>{{if .ExecCommand}}<mark>exec</mark>{{else}}{{.ShellName}}{{end}}</td>
|
||||||
<td>{{if .HumanScore}}{{if gt (derefFloat .HumanScore) 0.6}}<mark>{{formatScore .HumanScore}}</mark>{{else}}{{formatScore .HumanScore}}{{end}}{{else}}-{{end}}</td>
|
<td>{{if .HumanScore}}{{if gt (derefFloat .HumanScore) 0.6}}<mark>{{formatScore .HumanScore}}</mark>{{else}}{{formatScore .HumanScore}}{{end}}{{else}}-{{end}}</td>
|
||||||
<td>{{formatTime .ConnectedAt}}</td>
|
<td>{{formatTime .ConnectedAt}}</td>
|
||||||
<td>{{if .DisconnectedAt}}{{formatTime (derefTime .DisconnectedAt)}}{{else}}<mark>active</mark>{{end}}</td>
|
<td>{{if .DisconnectedAt}}{{formatTime (derefTime .DisconnectedAt)}}{{else}}<mark>active</mark>{{end}}</td>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<th>IP</th>
|
<th>IP</th>
|
||||||
<th>Country</th>
|
<th>Country</th>
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
<th>Shell</th>
|
<th>Type</th>
|
||||||
<th>Score</th>
|
<th>Score</th>
|
||||||
<th>Connected</th>
|
<th>Connected</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -14,11 +14,11 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{{range .}}
|
{{range .}}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="/sessions/{{.ID}}"><code>{{truncateID .ID}}</code></a></td>
|
<td><a href="/sessions/{{.ID}}"><code>{{truncateID .ID}}</code></a>{{if gt .EventCount 0}} <mark>replay</mark>{{end}}</td>
|
||||||
<td>{{.IP}}</td>
|
<td>{{.IP}}</td>
|
||||||
<td>{{.Country}}</td>
|
<td>{{.Country}}</td>
|
||||||
<td>{{.Username}}</td>
|
<td>{{.Username}}</td>
|
||||||
<td>{{.ShellName}}</td>
|
<td>{{if .ExecCommand}}<mark>exec</mark>{{else}}{{.ShellName}}{{end}}</td>
|
||||||
<td>{{if .HumanScore}}{{if gt (derefFloat .HumanScore) 0.6}}<mark>{{formatScore .HumanScore}}</mark>{{else}}{{formatScore .HumanScore}}{{end}}{{else}}-{{end}}</td>
|
<td>{{if .HumanScore}}{{if gt (derefFloat .HumanScore) 0.6}}<mark>{{formatScore .HumanScore}}</mark>{{else}}{{formatScore .HumanScore}}{{end}}{{else}}-{{end}}</td>
|
||||||
<td>{{formatTime .ConnectedAt}}</td>
|
<td>{{formatTime .ConnectedAt}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -340,6 +340,61 @@ func TestMetricsBearerToken(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTruncateCommand(t *testing.T) {
|
||||||
|
funcMap := templateFuncMap()
|
||||||
|
fn := funcMap["truncateCommand"].(func(string) string)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"short", "short"},
|
||||||
|
{"exactly fifty characters long! that is what it i.", "exactly fifty characters long! that is what it i."},
|
||||||
|
{"this string is definitely longer than fifty characters and should be truncated", "this string is definitely longer than fifty charac..."},
|
||||||
|
{"", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := fn(tt.input)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("truncateCommand(%q) = %q, want %q", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDashboardExecCommands(t *testing.T) {
|
||||||
|
store := storage.NewMemoryStore()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
id, err := store.CreateSession(ctx, "10.0.0.1", "root", "bash", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating session: %v", err)
|
||||||
|
}
|
||||||
|
if err := store.SetExecCommand(ctx, id, "uname -a"); err != nil {
|
||||||
|
t.Fatalf("setting exec command: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srv, err := NewServer(store, slog.Default(), nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewServer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("status = %d, want 200", w.Code)
|
||||||
|
}
|
||||||
|
body := w.Body.String()
|
||||||
|
if !strings.Contains(body, "Top Exec Commands") {
|
||||||
|
t.Error("response should contain 'Top Exec Commands'")
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "uname -a") {
|
||||||
|
t.Error("response should contain exec command 'uname -a'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStaticAssets(t *testing.T) {
|
func TestStaticAssets(t *testing.T) {
|
||||||
srv := newTestServer(t)
|
srv := newTestServer(t)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user