diff --git a/apiary.toml b/apiary.toml index 5124c5a..bb9fb84 100644 --- a/apiary.toml +++ b/apiary.toml @@ -25,7 +25,7 @@ ListenAddr = ":2222" # Throttle incoming and outgoing data per connection # Values are in bytes per second. Empty means no unlimited # Default: "" -ThrottleSpeed = 10240 +ThrottleSpeed = 10240.0 [Frontend] # Log level for SSH Honeypot diff --git a/honeypot/store/memory.go b/honeypot/store/memory.go index b27b926..b68fbc2 100644 --- a/honeypot/store/memory.go +++ b/honeypot/store/memory.go @@ -3,6 +3,7 @@ package store import ( "fmt" "sort" + "strings" "sync" "github.uio.no/torjus/apiary/models" @@ -116,6 +117,31 @@ func (ms *MemoryStore) statTotals() ([]StatsResult, error) { return stats, nil } +func (ms *MemoryStore) Query(query AttemptQuery) ([]models.LoginAttempt, error) { + var results []models.LoginAttempt + ms.lock.Lock() + defer ms.lock.Unlock() + + for _, la := range ms.attempts { + switch query.QueryType { + case AttemptQueryTypeIP: + if la.RemoteIP.String() == query.Query { + results = append(results, la) + } + case AttemptQueryTypePassword: + if strings.Contains(la.Password, query.Query) { + results = append(results, la) + } + case AttemptQueryTypeUsername: + if strings.Contains(la.Username, query.Query) { + results = append(results, la) + } + } + } + + return results, nil +} + func toResults(m map[string]int) []StatsResult { var results []StatsResult diff --git a/honeypot/store/postgres.go b/honeypot/store/postgres.go index 06a37d6..88a2b2c 100644 --- a/honeypot/store/postgres.go +++ b/honeypot/store/postgres.go @@ -158,3 +158,39 @@ func (s *PostgresStore) statsTotal(limit int) ([]StatsResult, error) { {Name: "TotalLoginAttempts", Count: attemptsCount}, }, nil } + +func (s *PostgresStore) Query(query AttemptQuery) ([]models.LoginAttempt, error) { + var stmt string + + switch query.QueryType { + case AttemptQueryTypeIP: + stmt = `SELECT id, date, remote_ip, username, password, client_version, connection_uuid, country + FROM login_attempts WHERE remote_ip = $1` + case AttemptQueryTypePassword: + stmt = `SELECT id, date, remote_ip, username, password, client_version, connection_uuid, country + FROM login_attempts WHERE password like '%$1%'` + case AttemptQueryTypeUsername: + stmt = `SELECT id, date, remote_ip, username, password, client_version, connection_uuid, country + FROM login_attempts WHERE username like '%$1%'` + default: + return nil, fmt.Errorf("Invalid query type") + } + + rows, err := s.db.Query(stmt, query.Query) + if err != nil { + return nil, fmt.Errorf("Unable to query database: %w", err) + } + defer rows.Close() + + var results []models.LoginAttempt + for rows.Next() { + var la models.LoginAttempt + if err := rows.Scan(&la.ID, &la.Date, &la.RemoteIP, &la.Username, &la.Password, &la.SSHClientVersion, &la.ConnectionUUID, &la.Country); err != nil { + return nil, fmt.Errorf("Unable to unmarshal data from database: %w", err) + } + results = append(results, la) + + } + + return results, nil +} diff --git a/honeypot/store/store.go b/honeypot/store/store.go index c3939bf..eb2f71e 100644 --- a/honeypot/store/store.go +++ b/honeypot/store/store.go @@ -13,13 +13,26 @@ const ( LoginStatsTotals LoginStats = "total" ) +type AttemptQueryType string + +const ( + AttemptQueryTypeUsername AttemptQueryType = "username" + AttemptQueryTypePassword AttemptQueryType = "password" + AttemptQueryTypeIP AttemptQueryType = "ip" +) + type StatsResult struct { Name string `json:"name"` Count int `json:"count"` } +type AttemptQuery struct { + QueryType AttemptQueryType + Query string +} type LoginAttemptStore interface { AddAttempt(l *models.LoginAttempt) error All() ([]models.LoginAttempt, error) Stats(statType LoginStats, limit int) ([]StatsResult, error) + Query(query AttemptQuery) ([]models.LoginAttempt, error) } diff --git a/web/server.go b/web/server.go index 3854f86..6325ff2 100644 --- a/web/server.go +++ b/web/server.go @@ -91,6 +91,7 @@ func NewServer(cfg config.FrontendConfig, hs *honeypot.HoneypotServer, store sto r.Use(middleware.SetHeader("Content-Type", "application/json")) r.Get("/stats", s.HandlerStats) r.Get("/stream", s.HandlerAttemptStream) + r.Get("/query", s.HandlerQuery) }) }) s.Handler = r @@ -185,6 +186,7 @@ func (s *Server) HandlerAttemptStream(w http.ResponseWriter, r *http.Request) { } func (s *Server) HandlerStats(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") statType := store.LoginStats(r.URL.Query().Get("type")) if statType == store.LoginStatsUndefined { statType = store.LoginStatsPasswords @@ -209,6 +211,36 @@ func (s *Server) HandlerStats(w http.ResponseWriter, r *http.Request) { s.ServerLogger.Debugf("Error encoding or writing response", "remote_ip", r.RemoteAddr, "error", err) } } +func (s *Server) HandlerQuery(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + queryType := r.URL.Query().Get("type") + query := r.URL.Query().Get("query") + + if query == "" || queryType == "" { + s.WriteAPIError(w, r, http.StatusBadRequest, "Invalid query or query type") + return + } + + aq := store.AttemptQuery{ + QueryType: store.AttemptQueryType(queryType), + Query: query, + } + + results, err := s.store.Query(aq) + if err != nil { + s.WriteAPIError(w, r, http.StatusInternalServerError, "Unable to perform query") + s.ServerLogger.Warnw("Error performing query", "error", err) + return + } + if results == nil { + results = []models.LoginAttempt{} + } + + encoder := json.NewEncoder(w) + if err := encoder.Encode(&results); err != nil { + s.ServerLogger.Warnw("Error writing query results", "error", err) + } +} type APIErrorResponse struct { Error string `json:"error"`