Initial commit

This commit is contained in:
2021-04-10 07:58:01 +02:00
commit 4bb38a797c
49 changed files with 12483 additions and 0 deletions

127
honeypot/store/memory.go Normal file
View File

@@ -0,0 +1,127 @@
package store
import (
"fmt"
"sort"
"sync"
"github.uio.no/torjus/apiary/models"
)
type MemoryStore struct {
lock sync.RWMutex
attempts []models.LoginAttempt
currentID int
}
type StatItem struct {
Key string
Count int
}
type StatItems []StatItem
func (ms *MemoryStore) AddAttempt(l *models.LoginAttempt) error {
ms.lock.Lock()
defer ms.lock.Unlock()
l.ID = ms.currentID + 1
ms.currentID = ms.currentID + 1
ms.attempts = append(ms.attempts, *l)
return nil
}
func (ms *MemoryStore) All() ([]models.LoginAttempt, error) {
return ms.attempts, nil
}
func (ms *MemoryStore) Stats(statType LoginStats, limit int) ([]StatsResult, error) {
counts := make(map[string]int)
if statType == LoginStatsTotals {
return ms.statTotals()
}
ms.lock.RLock()
defer ms.lock.RUnlock()
for _, a := range ms.attempts {
switch statType {
case LoginStatsPasswords:
counts[a.Password]++
case LoginStatsCountry:
counts[a.Country]++
case LoginStatsIP:
counts[a.RemoteIP.String()]++
case LoginStatsUsername:
counts[a.Username]++
default:
return nil, fmt.Errorf("Invalid stat type")
}
}
if limit < 1 {
return toResults(counts), nil
}
if limit >= len(counts) {
return toResults(counts), nil
}
var si StatItems
for key := range counts {
si = append(si, StatItem{Key: key, Count: counts[key]})
}
sort.Sort(si)
output := make(map[string]int)
for i := len(si) - 1; i > len(si)-limit-1; i-- {
output[si[i].Key] = si[i].Count
}
return toResults(output), nil
}
func (ss StatItems) Len() int {
return len(ss)
}
func (ss StatItems) Less(i, j int) bool {
return ss[i].Count < ss[j].Count
}
func (ss StatItems) Swap(i, j int) {
ss[i], ss[j] = ss[j], ss[i]
}
func (ms *MemoryStore) statTotals() ([]StatsResult, error) {
passwords := make(map[string]int)
usernames := make(map[string]int)
ips := make(map[string]int)
countries := make(map[string]int)
ms.lock.RLock()
defer ms.lock.RUnlock()
for _, val := range ms.attempts {
passwords[val.Password] += 1
usernames[val.Username] += 1
ips[val.RemoteIP.String()] += 1
countries[val.Country] += 1
}
stats := []StatsResult{
{Name: "UniquePasswords", Count: len(passwords)},
{Name: "UniqueUsernames", Count: len(usernames)},
{Name: "UniqueIPs", Count: len(ips)},
{Name: "UniqueCountries", Count: len(countries)},
{Name: "TotalLoginAttempts", Count: len(ms.attempts)},
}
return stats, nil
}
func toResults(m map[string]int) []StatsResult {
var results []StatsResult
for key, value := range m {
results = append(results, StatsResult{key, value})
}
return results
}

View File

@@ -0,0 +1,119 @@
package store
/*
func TestStatItems(t *testing.T) {
var tc = []struct {
Input StatItems
ExpectedOutput StatItems
}{
{
Input: StatItems{
{Key: "a", Count: 5},
{Key: "b", Count: 100},
{Key: "c", Count: 99},
{Key: "d", Count: 98},
{Key: "f", Count: 18},
},
ExpectedOutput: StatItems{
{Key: "a", Count: 5},
{Key: "f", Count: 18},
{Key: "d", Count: 98},
{Key: "c", Count: 99},
{Key: "b", Count: 100},
},
},
}
for _, testCase := range tc {
sort.Sort(testCase.Input)
for i := range testCase.Input {
if testCase.Input[i] != testCase.ExpectedOutput[i] {
t.Fatalf("Not sorted correctly")
}
}
}
}
func TestStats(t *testing.T) {
var exampleAttempts = []models.LoginAttempt{
{Username: "root", Password: "root", Country: "NO"},
{Username: "root", Password: "root", Country: "US"},
{Username: "user", Password: "passWord", Country: "US"},
{Username: "ibm", Password: "ibm", Country: "US"},
{Username: "ubnt", Password: "ubnt", Country: "GB"},
{Username: "ubnt", Password: "ubnt", Country: "FI"},
{Username: "root", Password: "root", Country: "CH"},
{Username: "ubnt", Password: "12345", Country: "DE"},
{Username: "oracle", Password: "oracle", Country: "FI"},
}
var tc = []struct {
Attempts []models.LoginAttempt
StatType LoginStats
Limit int
ExpectedOutput map[string]int
}{
{
Attempts: exampleAttempts,
StatType: LoginStatsPasswords,
Limit: 2,
ExpectedOutput: map[string]int{
"root": 3,
"ubnt": 2,
},
},
{
Attempts: exampleAttempts,
StatType: LoginStatsPasswords,
Limit: 1,
ExpectedOutput: map[string]int{
"root": 3,
},
},
{
Attempts: exampleAttempts,
StatType: LoginStatsCountry,
Limit: 2,
ExpectedOutput: map[string]int{
"US": 3,
"FI": 2,
},
},
{
Attempts: exampleAttempts,
StatType: LoginStatsCountry,
Limit: 0,
ExpectedOutput: map[string]int{
"US": 3,
"FI": 2,
"NO": 1,
"GB": 1,
"CH": 1,
"DE": 1,
},
},
}
for _, c := range tc {
ms := MemoryStore{}
for _, a := range c.Attempts {
if err := ms.AddAttempt(a); err != nil {
t.Fatalf("Unable to add attempt: %s", err)
}
}
stats, err := ms.Stats(c.StatType, c.Limit)
if err != nil {
t.Fatalf("Error getting stats: %s", err)
}
if len(stats) != len(c.ExpectedOutput) {
t.Fatalf("Stats have wrong length")
}
for key := range stats {
if c.ExpectedOutput[key] != stats[key] {
t.Fatalf("Stats does not match expected output")
}
}
}
}
*/

131
honeypot/store/postgres.go Normal file
View File

@@ -0,0 +1,131 @@
package store
import (
"database/sql"
"fmt"
_ "github.com/jackc/pgx/v4/stdlib"
"github.uio.no/torjus/apiary/models"
)
const DBSchema = `
CREATE TABLE IF NOT EXISTS login_attempts(
id serial PRIMARY KEY,
date timestamptz,
remote_ip inet,
username text,
password text,
client_version text,
connection_uuid uuid,
country varchar(2)
);
`
type PostgresStore struct {
db *sql.DB
}
func NewPostgresStore(dsn string) (*PostgresStore, error) {
db, err := sql.Open("pgx", dsn)
if err != nil {
return nil, err
}
rs := &PostgresStore{
db: db,
}
return rs, nil
}
func (s *PostgresStore) InitDB() error {
_, err := s.db.Exec(DBSchema)
return err
}
func (s *PostgresStore) AddAttempt(l *models.LoginAttempt) error {
stmt := `INSERT INTO
login_attempts(date, remote_ip, username, password, client_version, connection_uuid, country)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id;`
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
var id int
if err := tx.QueryRow(stmt, l.Date, l.RemoteIP.String(), l.Username, l.Password, l.SSHClientVersion, l.ConnectionUUID, l.Country).Scan(&id); err != nil {
return err
}
l.ID = id
return nil
}
func (s *PostgresStore) All() ([]models.LoginAttempt, error) {
stmt := `SELECT date, remote_ip, username, password, client_version, connection_uuid, country FROM login_attempts`
tx, err := s.db.Begin()
if err != nil {
return nil, err
}
rows, err := tx.Query(stmt)
if err != nil {
return nil, err
}
defer rows.Close()
var attempts []models.LoginAttempt
for rows.Next() {
var a models.LoginAttempt
if err := rows.Scan(&a.Date, &a.RemoteIP, &a.Username, &a.Password, &a.SSHClientVersion, &a.SSHClientVersion, &a.Country); err != nil {
return nil, err
}
attempts = append(attempts, a)
}
return attempts, nil
}
func (s *PostgresStore) Stats(statType LoginStats, limit int) ([]StatsResult, error) {
var stmt string
if statType == LoginStatsTotals {
return s.statsTotal(limit)
}
switch statType {
case LoginStatsCountry:
stmt = `select country, count(country) from login_attempts order by count desc`
case LoginStatsIP:
stmt = `select remote_ip, count(remote_ip) from login_attempts order by count desc`
case LoginStatsPasswords:
stmt = `select password, count(password) from login_attempts order by count desc`
case LoginStatsUsername:
stmt = `select username, count(username) from login_attempts order by count desc`
}
if limit > 0 {
stmt = fmt.Sprintf("%s limit %d", stmt, limit)
}
rows, err := s.db.Query(stmt)
if err != nil {
return nil, err
}
defer rows.Close()
var results []StatsResult
for rows.Next() {
var r StatsResult
if err := rows.Scan(&r.Name, &r.Count); err != nil {
return nil, err
}
results = append(results, r)
}
return results, nil
}
func (s *PostgresStore) statsTotal(limit int) ([]StatsResult, error) {
return nil, nil
}

25
honeypot/store/store.go Normal file
View File

@@ -0,0 +1,25 @@
package store
import "github.uio.no/torjus/apiary/models"
type LoginStats string
const (
LoginStatsUndefined LoginStats = ""
LoginStatsPasswords LoginStats = "password"
LoginStatsCountry LoginStats = "country"
LoginStatsIP LoginStats = "ips"
LoginStatsUsername LoginStats = "username"
LoginStatsTotals LoginStats = "totals"
)
type StatsResult struct {
Name string `json:"name"`
Count int `json:"count"`
}
type LoginAttemptStore interface {
AddAttempt(l *models.LoginAttempt) error
All() ([]models.LoginAttempt, error)
Stats(statType LoginStats, limit int) ([]StatsResult, error)
}