Update Notifications, Fix DB Cleanup, Error Handling, Move ScanResult to Component

This commit is contained in:
Evan Reichard 2021-03-21 12:28:02 -04:00
parent 0632c06978
commit 6b40111a3b
24 changed files with 347 additions and 172 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
**/__pycache__/ **/__pycache__/
**/.coverage **/.coverage
**/overseer.sqlite
src/overseer.egg-info/ src/overseer.egg-info/
build/ build/
dist/ dist/

View File

@ -23,11 +23,14 @@ def post_scans():
""" """
data = request.get_json() data = request.get_json()
if data is None or "target" not in data: if data is None or "target" not in data:
return {"error": "Missing 'target'"}, 422 return {"error": "Missing target"}, 422
if data["target"].strip() == "":
return {"error": "Invalid target"}, 422
scan_history = overseer.scan_manager.perform_scan(data["target"]) scan_history = overseer.scan_manager.perform_scan(data["target"])
if scan_history is None: if scan_history is None:
return {"error": "Unable to resolve hostname."}, 422 return {"error": "Unable to resolve hostname"}, 422
return __normalize_scan_results([scan_history], data["target"])[0] return __normalize_scan_results([scan_history], data["target"])[0]

View File

@ -59,7 +59,7 @@ class DatabaseConnector:
for stale in all_stale: for stale in all_stale:
stale.status = "FAILED" stale.status = "FAILED"
stale.message = "Stale history" stale.error = "Stale history"
session.commit() session.commit()
session.close() session.close()

View File

@ -124,7 +124,7 @@ class ScanManager:
Either a hostname or IP address of the endpoint to scan Either a hostname or IP address of the endpoint to scan
""" """
try: try:
target = socket.gethostbyname(target) socket.gethostbyname(target)
except socket.error: except socket.error:
return None return None

View File

@ -1 +0,0 @@
#overseer-websocket[data-v-7a0962f4]{height:18px;width:18px;border-radius:8px;position:absolute;left:25px;top:20px;z-index:-1}#overseer-header[data-v-7a0962f4]{background-color:#c9582c;width:100%;height:60px;position:fixed;top:0}#overseer-header a[data-v-7a0962f4]{color:inherit}#overseer-notifications[data-v-3073f05e]{height:calc(100% - 60px);width:320px;position:fixed;transition:.5s;right:0;top:0;overflow:scroll;margin-top:60px;z-index:1}.overseer-notification[data-v-3073f05e]{height:65px;transition:.5s;background-color:#e0e3de;border-radius:2px;text-align:start;padding:10px;margin:10px;cursor:pointer}#overseer-app{height:100%;width:100%;font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#3a4040}#overseer-body{height:calc(100% - 60px);width:100%;margin-top:60px;overflow:scroll;position:absolute;color:#eaece9}#overseer-body-center{min-height:100%;width:1000px;margin:0 auto;background-color:#343a46;box-shadow:inset 0 0 3px 3px #2b303a}body,html{height:100%;width:100%;margin:0;padding:0;background-color:#2b303a}#overseer-search[data-v-b2a880be]::-moz-placeholder{text-align:center}#overseer-search[data-v-b2a880be]:-ms-input-placeholder{text-align:center}#overseer-search[data-v-b2a880be]::placeholder{text-align:center}#overseer-search[data-v-b2a880be]{width:400px;height:40px;border-radius:5px;text-align:center;margin-top:400px;font-size:1rem;padding:.3rem .8rem;text-indent:0;outline:none;border:0 solid}.lds-roller[data-v-d7e11832]{display:inline-block;position:relative;width:80px;height:80px}.lds-roller div[data-v-d7e11832]{-webkit-animation:lds-roller-data-v-d7e11832 1.2s cubic-bezier(.5,0,.5,1) infinite;animation:lds-roller-data-v-d7e11832 1.2s cubic-bezier(.5,0,.5,1) infinite;transform-origin:40px 40px}.lds-roller div[data-v-d7e11832]:after{content:" ";display:block;position:absolute;width:7px;height:7px;border-radius:50%;background:#fff;margin:-4px 0 0 -4px}.lds-roller div[data-v-d7e11832]:first-child{-webkit-animation-delay:-36ms;animation-delay:-36ms}.lds-roller div[data-v-d7e11832]:first-child:after{top:63px;left:63px}.lds-roller div[data-v-d7e11832]:nth-child(2){-webkit-animation-delay:-72ms;animation-delay:-72ms}.lds-roller div[data-v-d7e11832]:nth-child(2):after{top:68px;left:56px}.lds-roller div[data-v-d7e11832]:nth-child(3){-webkit-animation-delay:-.108s;animation-delay:-.108s}.lds-roller div[data-v-d7e11832]:nth-child(3):after{top:71px;left:48px}.lds-roller div[data-v-d7e11832]:nth-child(4){-webkit-animation-delay:-.144s;animation-delay:-.144s}.lds-roller div[data-v-d7e11832]:nth-child(4):after{top:72px;left:40px}.lds-roller div[data-v-d7e11832]:nth-child(5){-webkit-animation-delay:-.18s;animation-delay:-.18s}.lds-roller div[data-v-d7e11832]:nth-child(5):after{top:71px;left:32px}.lds-roller div[data-v-d7e11832]:nth-child(6){-webkit-animation-delay:-.216s;animation-delay:-.216s}.lds-roller div[data-v-d7e11832]:nth-child(6):after{top:68px;left:24px}.lds-roller div[data-v-d7e11832]:nth-child(7){-webkit-animation-delay:-.252s;animation-delay:-.252s}.lds-roller div[data-v-d7e11832]:nth-child(7):after{top:63px;left:17px}.lds-roller div[data-v-d7e11832]:nth-child(8){-webkit-animation-delay:-.288s;animation-delay:-.288s}.lds-roller div[data-v-d7e11832]:nth-child(8):after{top:56px;left:12px}@-webkit-keyframes lds-roller-data-v-d7e11832{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes lds-roller-data-v-d7e11832{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}#overseer-scan[data-v-09bc5538]{text-align:left}#results[data-v-09bc5538]{width:500px;margin:150px auto 0 auto}.proto[data-v-09bc5538]{border-radius:4px;padding:0 2px;color:#0a282f;margin-left:5px}.scan-progress[data-v-09bc5538]{border:1px solid #000;border-radius:6px;overflow:hidden;position:relative;text-align:center;margin-bottom:5px}.scan-progress span[data-v-09bc5538]{position:absolute;font-size:.75em;right:48%;font-weight:900;top:-2px;color:#000}.scan-progress div[data-v-09bc5538]{height:12px;background-color:green;text-align:center;font-size:.6em;width:0}li[data-v-09bc5538]{display:flex;margin:10px 43px;width:80px}li span[data-v-09bc5538]{font-weight:700}ul[data-v-09bc5538]{display:flex;-moz-column-count:3;column-count:3;flex-wrap:wrap;flex-direction:row;list-style-type:none;padding:0}

View File

@ -0,0 +1 @@
#overseer-websocket[data-v-1f35cde8]{height:18px;width:18px;border-radius:8px;position:absolute;left:25px;top:20px;z-index:-1}#overseer-header[data-v-1f35cde8]{background-color:#c9582c;width:100%;height:60px;position:fixed;top:0}#overseer-header a[data-v-1f35cde8]{color:inherit}.scan-progress[data-v-2b9f4f85]{border:1px solid #000;border-radius:6px;overflow:hidden;position:relative;text-align:center;margin-bottom:5px}.scan-progress span[data-v-2b9f4f85]{position:absolute;font-size:.75em;right:48%;font-weight:900;top:-2px;color:#000}.scan-progress div[data-v-2b9f4f85]{height:12px;transition:1s;background-color:green;text-align:center;font-size:.6em;width:0}#overseer-notifications[data-v-7218f0ce]{height:calc(100% - 60px);width:320px;position:fixed;transition:.5s;right:0;top:0;overflow:scroll;margin-top:60px;z-index:1}.overseer-notification[data-v-7218f0ce]{transition:.5s;background-color:#e0e3de;border-radius:2px;text-align:start;padding:10px;margin:10px;cursor:pointer}#overseer-app{height:100%;width:100%;font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#3a4040}#overseer-body{height:calc(100% - 60px);width:100%;margin-top:60px;overflow:scroll;position:absolute;color:#eaece9}#overseer-body-center{min-height:100%;width:1000px;margin:0 auto;background-color:#343a46;box-shadow:inset 0 0 3px 3px #2b303a}body,html{height:100%;width:100%;margin:0;padding:0;background-color:#2b303a}.lds-roller[data-v-d7e11832]{display:inline-block;position:relative;width:80px;height:80px}.lds-roller div[data-v-d7e11832]{-webkit-animation:lds-roller-data-v-d7e11832 1.2s cubic-bezier(.5,0,.5,1) infinite;animation:lds-roller-data-v-d7e11832 1.2s cubic-bezier(.5,0,.5,1) infinite;transform-origin:40px 40px}.lds-roller div[data-v-d7e11832]:after{content:" ";display:block;position:absolute;width:7px;height:7px;border-radius:50%;background:#fff;margin:-4px 0 0 -4px}.lds-roller div[data-v-d7e11832]:first-child{-webkit-animation-delay:-36ms;animation-delay:-36ms}.lds-roller div[data-v-d7e11832]:first-child:after{top:63px;left:63px}.lds-roller div[data-v-d7e11832]:nth-child(2){-webkit-animation-delay:-72ms;animation-delay:-72ms}.lds-roller div[data-v-d7e11832]:nth-child(2):after{top:68px;left:56px}.lds-roller div[data-v-d7e11832]:nth-child(3){-webkit-animation-delay:-.108s;animation-delay:-.108s}.lds-roller div[data-v-d7e11832]:nth-child(3):after{top:71px;left:48px}.lds-roller div[data-v-d7e11832]:nth-child(4){-webkit-animation-delay:-.144s;animation-delay:-.144s}.lds-roller div[data-v-d7e11832]:nth-child(4):after{top:72px;left:40px}.lds-roller div[data-v-d7e11832]:nth-child(5){-webkit-animation-delay:-.18s;animation-delay:-.18s}.lds-roller div[data-v-d7e11832]:nth-child(5):after{top:71px;left:32px}.lds-roller div[data-v-d7e11832]:nth-child(6){-webkit-animation-delay:-.216s;animation-delay:-.216s}.lds-roller div[data-v-d7e11832]:nth-child(6):after{top:68px;left:24px}.lds-roller div[data-v-d7e11832]:nth-child(7){-webkit-animation-delay:-.252s;animation-delay:-.252s}.lds-roller div[data-v-d7e11832]:nth-child(7):after{top:63px;left:17px}.lds-roller div[data-v-d7e11832]:nth-child(8){-webkit-animation-delay:-.288s;animation-delay:-.288s}.lds-roller div[data-v-d7e11832]:nth-child(8):after{top:56px;left:12px}@-webkit-keyframes lds-roller-data-v-d7e11832{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes lds-roller-data-v-d7e11832{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}#overseer-search[data-v-7ef10590]::-moz-placeholder{text-align:center}#overseer-search[data-v-7ef10590]:-ms-input-placeholder{text-align:center}#overseer-search[data-v-7ef10590]::placeholder{text-align:center}#overseer-search[data-v-7ef10590]{width:400px;height:40px;border-radius:5px;text-align:center;font-size:1rem;padding:.3rem .8rem;text-indent:0;outline:none;border:0 solid}#overseer-websocket[data-v-7ef10590]{height:40px;width:40px;border-radius:20px;position:absolute;left:19px;bottom:66px}.error[data-v-7ef10590]{margin-top:5px;color:#b30000}#scan-status[data-v-7c05d396]{color:#eaece9;text-align:center}#sub-status[data-v-7c05d396]{text-align:center;width:100%;margin:-20px 0 10px 0;border-bottom:1px solid}.proto[data-v-7c05d396]{border-radius:4px;padding:0 2px;color:#0a282f;margin-left:5px}#overseer-scan[data-v-3b3475aa]{text-align:left}#results[data-v-3b3475aa]{width:500px;margin:150px auto 0 auto}li[data-v-3b3475aa]{display:flex;margin:10px 43px;width:80px}li span[data-v-3b3475aa]{font-weight:700}ul[data-v-3b3475aa]{display:flex;-moz-column-count:3;column-count:3;flex-wrap:wrap;flex-direction:row;list-style-type:none;padding:0}#scan-button[data-v-3b3475aa]{font-size:1em;background-color:#0e6a0e;padding:8px;border-radius:10px;float:right;font-weight:700;cursor:pointer;margin:8px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/static/favicon.ico"><title>overseer_client</title><link href="/static/css/app.a50f799c.css" rel="preload" as="style"><link href="/static/js/app.e5dd211e.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.cf069257.js" rel="preload" as="script"><link href="/static/css/app.a50f799c.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but overseer_client doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.cf069257.js"></script><script src="/static/js/app.e5dd211e.js"></script></body></html> <!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/static/favicon.ico"><title>Overseer</title><link href="/static/css/app.adf7b5c2.css" rel="preload" as="style"><link href="/static/js/app.a3974328.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.4fdbc4a4.js" rel="preload" as="script"><link href="/static/css/app.adf7b5c2.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but Overseer doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.4fdbc4a4.js"></script><script src="/static/js/app.a3974328.js"></script></body></html>

View File

@ -1,5 +1,5 @@
{ {
"name": "overseer_client", "name": "Overseer",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {

View File

@ -5,7 +5,7 @@
<div v-if="!this.$store.state.ws_connected" style="background-color: #B30000;" id="overseer-websocket"></div> <div v-if="!this.$store.state.ws_connected" style="background-color: #B30000;" id="overseer-websocket"></div>
<div style="float: left; margin: 8px 20px; font-weight: 700; font-size: 2em;">Overseer</div> <div style="float: left; margin: 8px 20px; font-weight: 700; font-size: 2em;">Overseer</div>
</router-link> </router-link>
<div style="float: right; margin: 10px 20px;"> <div v-on:click="toggleNotifications()" style="float: right; margin: 10px 20px; cursor: pointer;">
<p style="color: white; position: absolute; font-weight: 700; width: 35px; height: 35px; margin: 9px 0px; font-size: .8em;"> <p style="color: white; position: absolute; font-weight: 700; width: 35px; height: 35px; margin: 9px 0px; font-size: .8em;">
{{ notificationCount }} {{ notificationCount }}
</p> </p>
@ -23,6 +23,11 @@ export default {
notificationCount() { notificationCount() {
return this.$store.state.notifications.length return this.$store.state.notifications.length
} }
},
methods: {
toggleNotifications() {
this.$root.$emit("toggleNotifications");
}
} }
} }
</script> </script>

View File

@ -1,27 +1,50 @@
<template> <template>
<div id="overseer-notifications"> <div id="overseer-notifications" v-bind:style="deriveNotificationCSS(showNotifications)">
<div v-on:click="navigateToScan(item)" class="overseer-notification" v-for="item in items" :key="item.id"> <div v-on:click="navigateToScan(scan)" class="overseer-notification" v-for="scan in activeScans" :key="scan.id">
<b>Target: </b> {{ item.target }} <br> <div style="float: left; width: 100%; margin: 5px 0px;">
<b>Status: </b>{{ item.status }} <br> <b style="float: left;">{{ scan.target }}</b>
<span v-if="'total_progress' in item"> <b style="float: right;">{{ scan.status }}</b>
<b>Progress: </b>{{item.total_progress}} <br> </div>
</span> <ScanProgress
:percentage="scan.status =='COMPLETE' ? 100 : scan.total_progress"
style="margin-top: 5px;"
/>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import ScanProgress from '../components/ScanProgress.vue'
export default { export default {
name: 'OverseerNotifications', name: 'OverseerNotifications',
components: {
ScanProgress,
},
data() {
return {
showNotifications: true
}
},
computed: { computed: {
items() { activeScans() {
return this.$store.state.notifications return this.$store.state.notifications
} }
}, },
methods: { methods: {
navigateToScan(item) { navigateToScan(scan) {
this.$router.push({ path: `/scan/${item.target}/${item.id}` }) this.$router.push({ path: `/scan/${scan.target}/${scan.id}` })
},
deriveNotificationCSS(showNotifications) {
return {
"right": showNotifications ? "0px" : "-320px"
}
} }
},
mounted() {
this.$root.$on("toggleNotifications", () => {
this.showNotifications = !this.showNotifications;
});
} }
} }
</script> </script>
@ -40,7 +63,6 @@ export default {
} }
.overseer-notification { .overseer-notification {
height: 65px;
transition: 0.5s; transition: 0.5s;
background-color: #E0E3DE; background-color: #E0E3DE;
border-radius: 2px; border-radius: 2px;

View File

@ -0,0 +1,62 @@
<template>
<div class="scan-progress" >
<span style="right: 46.5%;">{{ title }}</span>
<div v-bind:style="scanProgress(percentage)"></div>
</div>
</template>
<script>
export default {
name: 'ScanProgress',
props: ["percentage", "title"],
methods: {
scanProgress(perc) {
// Calculate color based on percentage - https://gist.github.com/mlocati/7210513
let r, g, b = 0;
if (perc < 50) {
r = 255;
g = Math.round(5.1 * perc);
} else {
g = 255;
r = Math.round(510 - 5.10 * perc)
}
let h = r * 0x10000 + g * 0x100 + b * 0x1;
let hexColor = '#' + ('000000' + h.toString(16)).slice(-6);
return {
"width": perc + "%",
"backgroundColor": hexColor,
}
},
}
}
</script>
<style scoped>
.scan-progress {
border: 1px solid black;
border-radius: 6px;
overflow: hidden;
position: relative;
text-align: center;
margin-bottom: 5px;
}
.scan-progress span {
position: absolute;
font-size: .75em;
right: 48%;
font-weight: 900;
top: -2px;
color: black;
}
.scan-progress div {
height: 12px;
transition: 1s;
background-color: green;
text-align: center;
font-size: .6em;
width: 0%;
}
</style>

View File

@ -0,0 +1,78 @@
<template>
<div v-if="scan.status == 'FAILED' || error">
<h2 id="scan-status">Error</h2>
<h5 id="sub-status" style="color: #B30000; border-bottom: unset;">{{ error || scan.error }}</h5>
</div>
<div v-else-if="scan.status == 'LOADING' || loading">
<Loading style="margin: 0px auto; display: block;" />
<h2 id="scan-status">Loading...</h2>
</div>
<div v-else-if="scan.status == 'NO_RESULTS'">
<h2 id="scan-status">No Scans Found</h2>
</div>
<div v-else>
<h2 v-if="scan.status == 'IN_PROGRESS'" id="scan-status">Scanning in Progress</h2>
<h2 v-else id="scan-status">Scan Results</h2>
<h5 id="sub-status">{{ normalizeDate(scan.created_at) }}</h5>
<div v-if="scan.status == 'IN_PROGRESS'" >
<ScanProgress title="TCP" :percentage="scan.tcp_progress" />
<ScanProgress title="UDP" :percentage="scan.udp_progress" />
<ScanProgress title="Total" :percentage="scan.total_progress" />
</div>
<ul>
<li v-for="port in scan.results" :key="port">
<span>{{ port.split(" ")[0] }}</span>
<span class="proto" v-bind:style="dynamicProtocolStyle(port.split(' ')[1])" >{{ port.split(" ")[1] }}</span>
</li>
</ul>
</div>
</template>
<script>
import Loading from '../components/Loading.vue'
import ScanProgress from '../components/ScanProgress.vue'
export default {
name: 'ScanResult',
props: ["scan", "error", "loading"],
components: {
Loading,
ScanProgress,
},
methods: {
normalizeDate(dateISO) {
let parsedDate = new Date(dateISO);
return parsedDate.toDateString() + " " + parsedDate.toLocaleTimeString()
},
dynamicProtocolStyle(proto) {
return {
"backgroundColor": proto == "TCP" ? "#E7E6FF" : "#D5EFFF",
}
},
}
}
</script>
<style scoped>
#scan-status {
color: #EAECE9;
text-align: center;
}
#sub-status {
text-align: center;
width: 100%;
margin: -20px 0px 10px 0px;
border-bottom: 1px solid;
}
.proto {
border-radius: 4px;
padding: 0px 2px;
color: #0A282F;
margin-left: 5px;
}
</style>

View File

@ -2,6 +2,7 @@ import Vue from 'vue'
import VueRouter from 'vue-router' import VueRouter from 'vue-router'
import Home from '../views/Home.vue' import Home from '../views/Home.vue'
import Scan from '../views/Scan.vue' import Scan from '../views/Scan.vue'
import Search from '../views/Search.vue'
import NotFound from '../views/NotFound.vue' import NotFound from '../views/NotFound.vue'
Vue.use(VueRouter) Vue.use(VueRouter)
@ -22,6 +23,11 @@ const routes = [
name: 'Scan', name: 'Scan',
component: Scan component: Scan
}, },
{
path: '/search/:target',
name: 'Search',
component: Search
},
{ {
path: '*', path: '*',
name: 'NotFound', name: 'NotFound',

View File

@ -16,48 +16,64 @@ export default new Vuex.Store({
}, },
actions: { actions: {
getScansByTarget({ commit }, target){ getScansByTarget({ commit }, target){
fetch('/api/v1/scans/' + target) return fetch('/api/v1/scans/' + target)
.then(resp => resp.json()) .then(resp => resp.json())
.then(json => { .then(json => {
commit("SET_SCANS", { target, data: json.data }); commit("SET_TARGET_SCANS", { target, data: json.data });
}); });
}, },
performScan({ commit }, target) {
return fetch('/api/v1/scans', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ "target": target })
}).then(async resp => {
let scan = await resp.json()
commit("UPDATE_SCAN", { scan });
return scan;
});
}
}, },
mutations: { mutations: {
"SET_SCANS"(state, { target, data }) { "SET_TARGET_SCANS"(state, { target, data }) {
console.log("SET_SCANS: ", target, data)
Vue.set(state.scan_cache, target, data) Vue.set(state.scan_cache, target, data)
}, },
"UPDATE_SCAN"(state, { scan }) {
let target = scan.target;
if (!state.scan_cache[target]) {
Vue.set(state.scan_cache, target, [scan])
} else {
let matchedItem = state.scan_cache[target]
.find(item => item.id == scan.id);
if (matchedItem)
Object.keys(scan).forEach(key => {
Vue.set(matchedItem, key, scan[key])
});
else
state.scan_cache[target].unshift(scan);
}
},
"SOCKET_message"(state, scan) {
// Update progress queue
let matchedItem = state.notifications.find(item => item.id == scan.id);
if (matchedItem)
Object.keys(scan).forEach(key => {
Vue.set(matchedItem, key, scan[key])
});
else
state.notifications.push(scan);
// Update scan cache
this.commit("UPDATE_SCAN", { scan });
},
"SOCKET_connect"(state) { "SOCKET_connect"(state) {
state.ws_connected = true state.ws_connected = true
}, },
"SOCKET_disconnect"(state) { "SOCKET_disconnect"(state) {
state.ws_connected = false state.ws_connected = false
}, },
"SOCKET_message"(state, message) {
// Update progress queue
let matchedItem = state.notifications.find(item => item.id == message.id);
if (matchedItem)
Object.keys(message).forEach(key => {
Vue.set(matchedItem, key, message[key])
});
else
state.notifications.push(message);
// Update scan cache
if (!state.scan_cache[message.target]) {
Vue.set(state.scan_cache, message.target, [message])
} else {
let matchedItem = state.scan_cache[message.target]
.find(item => item.id == message.id);
if (matchedItem)
Object.keys(message).forEach(key => {
Vue.set(matchedItem, key, message[key])
});
else
state.scan_cache[message.target].unshift(message);
}
}
}, },
}) })

View File

@ -1,26 +1,45 @@
<template> <template>
<input @keyup.enter="onSubmit" id="overseer-search" placeholder="IP Address / Hostname" type="text"> <div>
<div style="padding: 250px 0px 40px; position: relative; width: 300px; margin: 0px auto;">
<div v-if="this.$store.state.ws_connected" style="background-color: #0E6A0E;" id="overseer-websocket"></div>
<div v-if="!this.$store.state.ws_connected" style="background-color: #B30000;" id="overseer-websocket"></div>
<div style="font-weight: 700; font-size: 4em; position: relative;">Overseer</div>
</div>
<input @keyup.enter="onSubmit" id="overseer-search" placeholder="IP Address / Hostname" type="text"><br>
<h5 v-if="error != null" class="error">{{ error }}</h5>
<Loading v-if="loading" style="margin-top: 50px;"/>
</div>
</template> </template>
<script> <script>
import Loading from '../components/Loading.vue'
export default { export default {
name: 'OverseerHome', name: 'OverseerHome',
components: {
Loading,
},
data() {
return {
error: null,
loading: false,
}
},
methods: { methods: {
onSubmit(event){ onSubmit(event){
fetch('/api/v1/scans', { this.loading = true;
method: 'POST', this.error = null;
headers: { this.$store.dispatch("performScan", event.target.value)
'Content-Type': 'application/json' .then(scan => {
}, if (scan.error)
body: JSON.stringify({ "target": event.target.value }) throw new Error(scan.error)
}).then(resp => resp.json()).then(json => { this.$router.push({ path: `/scan/${event.target.value}/${scan.id}` })
// TODO: HANDLE POTENTIAL ERROR }).catch(err => {
console.log("RESPONSE: ", json); this.error = err;
this.$router.push({ path: `/scan/${event.target.value}` }) }).finally(() => {
}).catch(err => { this.loading = false;
// TODO: HANDLE ERROR });
console.log("ERROR: ", err);
});
} }
} }
} }
@ -30,16 +49,30 @@ export default {
#overseer-search::placeholder{ #overseer-search::placeholder{
text-align: center; text-align: center;
} }
#overseer-search { #overseer-search {
width: 400px; width: 400px;
height: 40px; height: 40px;
border-radius: 5px; border-radius: 5px;
text-align: center; text-align: center;
margin-top: 400px;
font-size: 1rem; font-size: 1rem;
padding: .3rem .8rem; padding: .3rem .8rem;
text-indent: 0px; text-indent: 0px;
outline: none; outline: none;
border: 0px solid; border: 0px solid;
} }
#overseer-websocket {
height: 40px;
width: 40px;
border-radius: 20px;
position: absolute;
left: 19px;
bottom: 66px;
}
.error {
margin-top: 5px;
color: #B30000;
}
</style> </style>

View File

@ -1,79 +1,43 @@
<template> <template>
<div id="overseer-scan" style="padding: 50px;"> <div id="overseer-scan" style="padding: 50px;">
<h1 style="font-size: 2.5em; margin: 0px;">{{ $route.params.target }}</h1> <h1 style="font-size: 2.5em; margin: 0px; float: left;">{{ $route.params.target }}</h1>
<span v-if="getRequestedScan.status == 'COMPLETE'" v-on:click="performScan()" id="scan-button">Scan Again</span>
<div id="results"> <div id="results">
<div v-if="getRequestedScan == 'LOADING'"> <ScanResult :scan="getRequestedScan" :error="error" :loading="loading"/>
<Loading/>
<h2 style="color: #EAECE9; text-align: center;">Loading...</h2>
</div>
<div v-else-if="getRequestedScan == 'NO_RESULTS'">
<h2 style="color: #EAECE9; text-align: center;">No Scans Found</h2>
</div>
<div v-else>
<h2 v-if="getRequestedScan.status == 'IN_PROGRESS'" style="color: #EAECE9; text-align: center;">Scanning in Progress</h2>
<h2 v-else style="color: #EAECE9; text-align: center;">Scan Results</h2>
<h5 style="text-align: center; width: 100%; margin: -20px 0px 10px 0px; border-bottom: 1px solid;">{{ normalizeDate(getRequestedScan.created_at) }}</h5>
<div class="scan-progress" v-if="getRequestedScan.status == 'IN_PROGRESS'" >
<span>TCP</span>
<div v-bind:style="scanProgress(getRequestedScan.tcp_progress)"></div>
</div>
<div class="scan-progress" v-if="getRequestedScan.status == 'IN_PROGRESS'" >
<span>UDP</span>
<div v-bind:style="scanProgress(getRequestedScan.udp_progress)"></div>
</div>
<div class="scan-progress" v-if="getRequestedScan.status == 'IN_PROGRESS'" >
<span style="right: 46.5%;">TOTAL</span>
<div v-bind:style="scanProgress(getRequestedScan.total_progress)"></div>
</div>
<ul>
<li v-for="port in getRequestedScan.results" :key="port">
<span>{{ port.split(" ")[0] }}</span>
<span class="proto" v-bind:style="dynamicProtocolStyle(port.split(' ')[1])" >{{ port.split(" ")[1] }}</span>
</li>
</ul>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Loading from '../components/Loading.vue' import ScanResult from '../components/ScanResult.vue'
export default { export default {
name: 'Scan', name: 'Scan',
components: { components: {
Loading, ScanResult,
},
data() {
return {
error: null,
loading: false
}
}, },
methods: { methods: {
scanProgress(perc) { performScan() {
// Calculate color based on percentage - https://gist.github.com/mlocati/7210513 this.error = null;
let r, g, b = 0; this.loading = true;
if (perc < 50) {
r = 255;
g = Math.round(5.1 * perc);
} else {
g = 255;
r = Math.round(510 - 5.10 * perc)
}
let h = r * 0x10000 + g * 0x100 + b * 0x1;
let hexColor = '#' + ('000000' + h.toString(16)).slice(-6);
return { this.$store.dispatch("performScan", this.$route.params.target)
"width": perc + "%", .then(scan => {
"backgroundColor": hexColor, if (scan.error)
} throw new Error(scan.error)
}, this.$router.push({ path: `/scan/${this.$route.params.target}/${scan.id}` })
dynamicProtocolStyle(proto) { }).catch(err => {
return { this.error = err;
"backgroundColor": proto == "TCP" ? "#E7E6FF" : "#D5EFFF", }).finally(() => {
} this.loading = false;
}, });
normalizeDate(dateISO) {
let parsedDate = new Date(dateISO);
return parsedDate.toDateString() + " " + parsedDate.toLocaleTimeString()
} }
}, },
computed: { computed: {
@ -83,15 +47,16 @@ export default {
let scanID = this.$route.params.scan_id; let scanID = this.$route.params.scan_id;
if (!scanCache[target]) if (!scanCache[target])
return "LOADING" return { "status": "LOADING" }
if (scanID) if (scanID)
return scanCache[target].find(item => item.id == scanID); return scanCache[target]
.find(item => item.id == scanID) || { "status": "NO_RESULTS" };
else else
return scanCache[target][0] || "NO_RESULTS"; return scanCache[target][0] || { "status": "NO_RESULTS" };
} }
}, },
created(){ mounted(){
this.$store.dispatch("getScansByTarget", this.$route.params.target); this.$store.dispatch("getScansByTarget", this.$route.params.target);
} }
} }
@ -107,39 +72,6 @@ export default {
margin: 150px auto 0px auto; margin: 150px auto 0px auto;
} }
.proto {
border-radius: 4px;
padding: 0px 2px;
color: #0A282F;
margin-left: 5px;
}
.scan-progress {
border: 1px solid black;
border-radius: 6px;
overflow: hidden;
position: relative;
text-align: center;
margin-bottom: 5px;
}
.scan-progress span {
position: absolute;
font-size: .75em;
right: 48%;
font-weight: 900;
top: -2px;
color: black;
}
.scan-progress div {
height: 12px;
background-color: green;
text-align: center;
font-size: .6em;
width: 0%;
}
li { li {
display: flex; display: flex;
margin: 10px 43px; margin: 10px 43px;
@ -158,4 +90,15 @@ ul {
list-style-type: none; list-style-type: none;
padding: 0px; padding: 0px;
} }
#scan-button {
font-size: 1em;
background-color: #0E6A0E;
padding: 8px;
border-radius: 10px;
float: right;
font-weight: 700;
cursor: pointer;
margin: 8px;
}
</style> </style>

View File

@ -0,0 +1,6 @@
<template>
<div style="padding-top: 300px;">
<h1>Error</h1>
<h4>404 Page Not Found</h4>
</div>
</template>