8 Commits

12 changed files with 248 additions and 5 deletions

View File

@@ -5,7 +5,7 @@ NAME = apiary
INSTALL_PREFIX ?= /usr/local INSTALL_PREFIX ?= /usr/local
VERSION = $(shell cat version.go |grep "Version"| cut -d "=" -f2| tr -d "\" ") VERSION = $(shell cat version.go |grep "var Version"| cut -d "=" -f2| tr -d "\" ")
ARCH = $(shell go env | grep GOHOSTARCH | cut -d"=" -f2 | tr -d "\"") ARCH = $(shell go env | grep GOHOSTARCH | cut -d"=" -f2 | tr -d "\"")
OS = $(shell go env | grep GOHOSTOS | cut -d"=" -f2 | tr -d "\"") OS = $(shell go env | grep GOHOSTOS | cut -d"=" -f2 | tr -d "\"")
GIT_COMMIT := $(shell git rev-parse --short HEAD) GIT_COMMIT := $(shell git rev-parse --short HEAD)

View File

@@ -25,7 +25,7 @@ ListenAddr = ":2222"
# Throttle incoming and outgoing data per connection # Throttle incoming and outgoing data per connection
# Values are in bytes per second. Empty means no unlimited # Values are in bytes per second. Empty means no unlimited
# Default: "" # Default: ""
ThrottleSpeed = 10240 ThrottleSpeed = 10240.0
[Frontend] [Frontend]
# Log level for SSH Honeypot # Log level for SSH Honeypot

View File

@@ -23,7 +23,7 @@ import (
func main() { func main() {
app := &cli.App{ app := &cli.App{
Name: "apiary", Name: "apiary",
Version: apiary.Version, Version: apiary.FullVersion(),
Authors: []*cli.Author{ Authors: []*cli.Author{
{ {
Name: "Torjus Håkestad", Name: "Torjus Håkestad",

View File

@@ -3,6 +3,7 @@ package store
import ( import (
"fmt" "fmt"
"sort" "sort"
"strings"
"sync" "sync"
"github.uio.no/torjus/apiary/models" "github.uio.no/torjus/apiary/models"
@@ -116,6 +117,31 @@ func (ms *MemoryStore) statTotals() ([]StatsResult, error) {
return stats, nil 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 { func toResults(m map[string]int) []StatsResult {
var results []StatsResult var results []StatsResult

View File

@@ -3,6 +3,7 @@ package store
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"net"
_ "github.com/jackc/pgx/v4/stdlib" _ "github.com/jackc/pgx/v4/stdlib"
"github.uio.no/torjus/apiary/models" "github.uio.no/torjus/apiary/models"
@@ -158,3 +159,44 @@ func (s *PostgresStore) statsTotal(limit int) ([]StatsResult, error) {
{Name: "TotalLoginAttempts", Count: attemptsCount}, {Name: "TotalLoginAttempts", Count: attemptsCount},
}, nil }, nil
} }
func (s *PostgresStore) Query(query AttemptQuery) ([]models.LoginAttempt, error) {
var stmt string
queryString := query.Query
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`
queryString = fmt.Sprintf("%%%s%%", queryString)
case AttemptQueryTypeUsername:
stmt = `SELECT id, date, remote_ip, username, password, client_version, connection_uuid, country
FROM login_attempts WHERE username like $1`
queryString = fmt.Sprintf("%%%s%%", queryString)
default:
return nil, fmt.Errorf("Invalid query type")
}
rows, err := s.db.Query(stmt, queryString)
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
var ipString string
if err := rows.Scan(&la.ID, &la.Date, &ipString, &la.Username, &la.Password, &la.SSHClientVersion, &la.ConnectionUUID, &la.Country); err != nil {
return nil, fmt.Errorf("Unable to unmarshal data from database: %w", err)
}
la.RemoteIP = net.ParseIP(ipString)
results = append(results, la)
}
return results, nil
}

View File

@@ -13,13 +13,26 @@ const (
LoginStatsTotals LoginStats = "total" LoginStatsTotals LoginStats = "total"
) )
type AttemptQueryType string
const (
AttemptQueryTypeUsername AttemptQueryType = "username"
AttemptQueryTypePassword AttemptQueryType = "password"
AttemptQueryTypeIP AttemptQueryType = "ip"
)
type StatsResult struct { type StatsResult struct {
Name string `json:"name"` Name string `json:"name"`
Count int `json:"count"` Count int `json:"count"`
} }
type AttemptQuery struct {
QueryType AttemptQueryType
Query string
}
type LoginAttemptStore interface { type LoginAttemptStore interface {
AddAttempt(l *models.LoginAttempt) error AddAttempt(l *models.LoginAttempt) error
All() ([]models.LoginAttempt, error) All() ([]models.LoginAttempt, error)
Stats(statType LoginStats, limit int) ([]StatsResult, error) Stats(statType LoginStats, limit int) ([]StatsResult, error)
Query(query AttemptQuery) ([]models.LoginAttempt, error)
} }

View File

@@ -1,4 +1,17 @@
package apiary package apiary
var Version = "v0.1.1" import (
"fmt"
"runtime"
)
var Version = "v0.1.4"
var Build string var Build string
func FullVersion() string {
if Build != "" {
return fmt.Sprintf("%s-%s (%s)", Version, Build, runtime.Version())
}
return fmt.Sprintf("%s (%s)", Version, runtime.Version())
}

View File

@@ -18,6 +18,7 @@
<b-nav-item :to="'/'">Home</b-nav-item> <b-nav-item :to="'/'">Home</b-nav-item>
<b-nav-item :to="'/stats'">Stats</b-nav-item> <b-nav-item :to="'/stats'">Stats</b-nav-item>
<b-nav-item :to="'/attempts'">Attempts</b-nav-item> <b-nav-item :to="'/attempts'">Attempts</b-nav-item>
<b-nav-item :to="'/search'">Search</b-nav-item>
</b-navbar-nav> </b-navbar-nav>
</b-collapse> </b-collapse>
</b-navbar> </b-navbar>

View File

@@ -8,7 +8,7 @@
hover hover
:items="attempts" :items="attempts"
:fields="fields" :fields="fields"
></b-table> />
</p> </p>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,81 @@
<template>
<div class="search-result-container">
<h1>Search</h1>
<b-form @submit="onSubmit">
<b-form-input v-model="searchString" placeholder="" />
</b-form>
<b-table
class="search-results-table"
responsive="md"
striped
hover
:items="attempts"
:fields="fields"
/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { LoginAttempt } from '@/apiary/apiary';
import { BvTableFieldArray, BvTableFormatterCallback } from 'bootstrap-vue';
import Axios from 'axios';
@Component
export default class SearchResult extends Vue {
attempts: LoginAttempt[];
searchString: string;
constructor() {
super();
this.attempts = [];
this.searchString = '';
}
fields: BvTableFieldArray = [
{
key: 'date',
sortable: true,
formatter: (value: string): string => {
const d = new Date(value);
// This seems stupid...
return d.toTimeString().split(' ')[0];
},
sortByFormatted: false,
},
{
key: 'username',
},
{
key: 'password',
},
{
key: 'remoteIP',
sortable: true,
},
{
key: 'country',
},
];
onSubmit(event: Event) {
event.preventDefault();
console.log(this.searchString);
const resp = Axios.get<LoginAttempt[]>(
`/api/query?query=${this.searchString}`,
);
resp.then((r) => {
this.attempts = r.data;
});
}
}
</script>
<style lang="scss" scoped>
.search-results-table {
margin-top: 50px;
}
</style>

View File

@@ -4,6 +4,7 @@ import VueRouter, { RouteConfig } from 'vue-router';
import AttemptList from '@/components/AttemptList.vue'; import AttemptList from '@/components/AttemptList.vue';
import Home from '@/components/Home.vue'; import Home from '@/components/Home.vue';
import Stats from '@/components/Stats.vue'; import Stats from '@/components/Stats.vue';
import SearchResult from '@/components/SearchResult.vue';
import 'bootstrap/dist/css/bootstrap.css'; import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue/dist/bootstrap-vue.css'; import 'bootstrap-vue/dist/bootstrap-vue.css';
import '@fontsource/rubik'; import '@fontsource/rubik';
@@ -38,6 +39,11 @@ const routes: Array<RouteConfig> = [
name: 'Stats', name: 'Stats',
component: Stats, component: Stats,
}, },
{
path: '/search',
name: 'Search',
component: SearchResult,
}
]; ];
const router = new VueRouter({ const router = new VueRouter({

View File

@@ -14,6 +14,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/google/uuid" "github.com/google/uuid"
"github.uio.no/torjus/apiary"
"github.uio.no/torjus/apiary/config" "github.uio.no/torjus/apiary/config"
"github.uio.no/torjus/apiary/honeypot" "github.uio.no/torjus/apiary/honeypot"
"github.uio.no/torjus/apiary/honeypot/store" "github.uio.no/torjus/apiary/honeypot/store"
@@ -91,6 +92,7 @@ func NewServer(cfg config.FrontendConfig, hs *honeypot.HoneypotServer, store sto
r.Use(middleware.SetHeader("Content-Type", "application/json")) r.Use(middleware.SetHeader("Content-Type", "application/json"))
r.Get("/stats", s.HandlerStats) r.Get("/stats", s.HandlerStats)
r.Get("/stream", s.HandlerAttemptStream) r.Get("/stream", s.HandlerAttemptStream)
r.Get("/query", s.HandlerQuery)
}) })
}) })
s.Handler = r s.Handler = r
@@ -150,6 +152,7 @@ func (s *Server) HandlerAttemptStream(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive") w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Server", apiary.FullVersion())
id, ch := s.addAttemptListener() id, ch := s.addAttemptListener()
defer s.closeAttemptListener(id) defer s.closeAttemptListener(id)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@@ -185,6 +188,7 @@ func (s *Server) HandlerAttemptStream(w http.ResponseWriter, r *http.Request) {
} }
func (s *Server) HandlerStats(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")) statType := store.LoginStats(r.URL.Query().Get("type"))
if statType == store.LoginStatsUndefined { if statType == store.LoginStatsUndefined {
statType = store.LoginStatsPasswords statType = store.LoginStatsPasswords
@@ -209,6 +213,63 @@ func (s *Server) HandlerStats(w http.ResponseWriter, r *http.Request) {
s.ServerLogger.Debugf("Error encoding or writing response", "remote_ip", r.RemoteAddr, "error", err) 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 == "" {
s.WriteAPIError(w, r, http.StatusBadRequest, "Invalid query or query type")
return
}
results := []models.LoginAttempt{}
if queryType == "" {
uq := store.AttemptQuery{
QueryType: store.AttemptQueryType(store.AttemptQueryTypePassword),
Query: query,
}
pq := store.AttemptQuery{
QueryType: store.AttemptQueryType(store.AttemptQueryTypeUsername),
Query: query,
}
userResults, err := s.store.Query(uq)
if err != nil {
s.WriteAPIError(w, r, http.StatusInternalServerError, "Unable to perform query")
s.ServerLogger.Warnw("Error performing query", "error", err)
return
}
passwordResults, err := s.store.Query(pq)
if err != nil {
s.WriteAPIError(w, r, http.StatusInternalServerError, "Unable to perform query")
s.ServerLogger.Warnw("Error performing query", "error", err)
return
}
results = append(results, userResults...)
results = append(results, passwordResults...)
} else {
aq := store.AttemptQuery{
QueryType: store.AttemptQueryType(queryType),
Query: query,
}
queryResults, 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
}
results = append(results, queryResults...)
}
encoder := json.NewEncoder(w)
if err := encoder.Encode(&results); err != nil {
s.ServerLogger.Warnw("Error writing query results", "error", err)
}
}
type APIErrorResponse struct { type APIErrorResponse struct {
Error string `json:"error"` Error string `json:"error"`