Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
baa3990d38 | |||
248735710a | |||
c1a1bf0b03 | |||
82d07eaaf4 | |||
a2ffbad4a3 | |||
f4afd8df62 | |||
946bdd2a6a | |||
6c20033bcc |
2
Makefile
2
Makefile
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
@@ -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",
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
15
version.go
15
version.go
@@ -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())
|
||||||
|
}
|
||||||
|
@@ -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>
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
hover
|
hover
|
||||||
:items="attempts"
|
:items="attempts"
|
||||||
:fields="fields"
|
:fields="fields"
|
||||||
></b-table>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
81
web/frontend/src/components/SearchResult.vue
Normal file
81
web/frontend/src/components/SearchResult.vue
Normal 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>
|
@@ -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({
|
||||||
|
@@ -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"`
|
||||||
|
Reference in New Issue
Block a user