Consistency, Routing, Progress, Styling

This commit is contained in:
Evan Reichard 2021-03-20 19:26:20 -04:00
parent 3d1e945fe1
commit 6d4cc4a11b
26 changed files with 544 additions and 122 deletions

View File

@ -25,11 +25,11 @@ def post_scans():
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
history_id = overseer.scan_manager.perform_scan(data["target"]) scan_history = overseer.scan_manager.perform_scan(data["target"])
if history_id is None: if scan_history is None:
return {"error": "Unable to resolve hostname."}, 422 return {"error": "Unable to resolve hostname."}, 422
return {"status": "started", "id": history_id} return __normalize_scan_results([scan_history], data["target"])[0]
@api.route("/scans/<string:target>", methods=["GET"]) @api.route("/scans/<string:target>", methods=["GET"])
@ -51,7 +51,7 @@ def get_scans_by_target(target):
page, hostname=target page, hostname=target
) )
return {"data": __normalize_scan_results(scan_results)} return {"data": __normalize_scan_results(scan_results, target)}
@api.route("/search", methods=["GET"]) @api.route("/search", methods=["GET"])
@ -66,13 +66,15 @@ def get_scans(search):
return "SEARCH PLACEHOLDER" return "SEARCH PLACEHOLDER"
def __normalize_scan_results(scan_results): def __normalize_scan_results(scan_results, target):
return list( return list(
map( map(
lambda x: { lambda x: {
"results": x.results.split(","), "id": x.id,
"created_at": x.created_at, "target": target,
"status": x.status, "status": x.status,
"results": x.results.split(",") if x.results != "" else [],
"created_at": x.created_at.isoformat(),
"error": x.error, "error": x.error,
}, },
scan_results, scan_results,

View File

@ -11,6 +11,14 @@ def main_entry():
return make_response(render_template("index.html")) return make_response(render_template("index.html"))
@app.route("/<path:path>", methods=["GET"])
def catch_all(path):
"""
Necessary due to client side SPA route handling.
"""
return make_response(render_template("index.html"))
@app.route("/static/<path:path>") @app.route("/static/<path:path>")
def static_resources(path): def static_resources(path):
""" """

View File

@ -17,18 +17,22 @@ class ScanManager:
def __broadcast_thread(self): def __broadcast_thread(self):
while not self.pending_shutdown: while not self.pending_shutdown:
time.sleep(5) time.sleep(1)
if len(self.active_scans) == 0: if len(self.active_scans) == 0:
continue continue
for scan in self.active_scans: for scan in self.active_scans:
# WebSocket progress # WebSocket progress
total_progress = (scan.tcp_progress + scan.udp_progress) / 2 total_progress = (scan.tcp_progress + scan.udp_progress) / 2
results = scan.get_results()
overseer.api.send_websocket_event( overseer.api.send_websocket_event(
{ {
"id": scan.history_id, "id": scan.scan_history.id,
"target": scan.target, "target": scan.target,
"status": "RUNNING", "status": "IN_PROGRESS",
"results": results,
"created_at": scan.scan_history.created_at.isoformat(),
"error": scan.scan_history.error,
"tcp_progress": scan.tcp_progress, "tcp_progress": scan.tcp_progress,
"udp_progress": scan.udp_progress, "udp_progress": scan.udp_progress,
"total_progress": round(total_progress), "total_progress": round(total_progress),
@ -38,25 +42,20 @@ class ScanManager:
if scan.tcp_progress + scan.udp_progress != 200: if scan.tcp_progress + scan.udp_progress != 200:
continue continue
# Combine ports
results = list(map(lambda x: "%s UDP" % x, scan.udp_results))
results.extend(
list(map(lambda x: "%s TCP" % x, scan.tcp_results))
) # noqa: E501
results.sort()
# Update database # Update database
overseer.database.update_scan_result( scan_history = overseer.database.update_scan_result(
scan.history_id, "COMPLETE", results=results scan.scan_history.id, "COMPLETE", results=results
) )
# WebSocket completion # WebSocket completion
overseer.api.send_websocket_event( overseer.api.send_websocket_event(
{ {
"id": scan.history_id, "id": scan_history.id,
"target": scan.target, "target": scan.target,
"status": "COMPLETE", "status": scan_history.status,
"results": results, "results": results,
"created_at": scan_history.created_at.isoformat(),
"error": scan_history.error,
} }
) )
@ -72,9 +71,12 @@ class ScanManager:
return list( return list(
map( map(
lambda x: { lambda x: {
"id": x.history_id, "id": x.scan_history.id,
"target": x.target, "target": x.target,
"status": "IN_PROGRESS", "status": "IN_PROGRESS",
"results": x.get_results(),
"created_at": x.scan_history.created_at.isoformat(),
"error": x.scan_history.error,
"tcp_progress": x.tcp_progress, "tcp_progress": x.tcp_progress,
"udp_progress": x.udp_progress, "udp_progress": x.udp_progress,
"total_progress": round( "total_progress": round(
@ -100,10 +102,10 @@ class ScanManager:
"IN_PROGRESS", hostname=target "IN_PROGRESS", hostname=target
) )
new_scan = Scanner(target, scan_history.id) new_scan = Scanner(target, scan_history)
new_scan.start() new_scan.start()
self.active_scans.append(new_scan) self.active_scans.append(new_scan)
return scan_history.id return scan_history
def is_ip(self, target): def is_ip(self, target):
try: try:
@ -114,10 +116,10 @@ class ScanManager:
class Scanner(Thread): class Scanner(Thread):
def __init__(self, target, history_id): def __init__(self, target, scan_history):
Thread.__init__(self) Thread.__init__(self)
self.target = target self.target = target
self.history_id = history_id self.scan_history = scan_history
self.port_count = 1000 self.port_count = 1000
@ -181,6 +183,14 @@ class Scanner(Thread):
udp_thread.join() udp_thread.join()
return {"TCP": self.tcp_results, "UDP": self.udp_results} return {"TCP": self.tcp_results, "UDP": self.udp_results}
def get_results(self):
results = list(map(lambda x: "%s UDP" % x, self.udp_results))
results.extend(
list(map(lambda x: "%s TCP" % x, self.tcp_results))
) # noqa: E501
results.sort()
return results
def __scan_tcp(self): def __scan_tcp(self):
for port in range(1, self.port_count): for port in range(1, self.port_count):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

View File

@ -0,0 +1 @@
#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

@ -1 +0,0 @@
#overseer-body[data-v-352462f2]{width:100%;height:100%}#center[data-v-352462f2]{margin:0 auto;width:1000px;height:100%;background-color:#343a46;box-shadow:inset 0 0 3px 3px #2b303a}#overseer-search[data-v-352462f2]::-moz-placeholder{text-align:center}#overseer-search[data-v-352462f2]:-ms-input-placeholder{text-align:center}#overseer-search[data-v-352462f2]::placeholder{text-align:center}#overseer-search[data-v-352462f2]{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}#overseer-header[data-v-1f75c2e8]{background-color:#c9582c;width:100%;height:60px;position:fixed;top:0}#overseer-notifications[data-v-47117d80]{height:calc(100% - 60px);width:320px;position:fixed;transition:.5s;right:0;overflow:scroll;margin-top:60px}.overseer-notification[data-v-47117d80]{height:65px;transition:.5s;background-color:#e0e3de;border-radius:2px;text-align:start;padding:10px;margin:10px}#overseer-app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#3a4040;height:100%}body,html{height:100%;width:100%;margin:0;background-color:#2b303a}

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.efe1070d.css" rel="preload" as="style"><link href="/static/js/app.b2809d88.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.6130e7bc.js" rel="preload" as="script"><link href="/static/css/app.efe1070d.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.6130e7bc.js"></script><script src="/static/js/app.b2809d88.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_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>

View File

@ -18,6 +18,7 @@
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-service": "~4.5.0", "@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"eslint": "^6.7.2", "eslint": "^6.7.2",

View File

@ -2,19 +2,21 @@
<div id="overseer-app"> <div id="overseer-app">
<OverseerHeader/> <OverseerHeader/>
<OverseerNotifications/> <OverseerNotifications/>
<OverseerHome/> <div id="overseer-body">
<div id="overseer-body-center">
<router-view/>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import OverseerHome from './components/OverseerHome.vue'
import OverseerHeader from './components/OverseerHeader.vue' import OverseerHeader from './components/OverseerHeader.vue'
import OverseerNotifications from './components/OverseerNotifications.vue' import OverseerNotifications from './components/OverseerNotifications.vue'
export default { export default {
name: 'App', name: 'App',
components: { components: {
OverseerHome,
OverseerHeader, OverseerHeader,
OverseerNotifications OverseerNotifications
} }
@ -23,18 +25,38 @@ export default {
<style> <style>
#overseer-app { #overseer-app {
height: 100%;
width: 100%;
font-family: Avenir, Helvetica, Arial, sans-serif; font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
text-align: center; text-align: center;
color: #3A4040; color: #3A4040;
height: 100%;
} }
#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: 0px auto;
background-color: #343A46;
box-shadow: 0 0 3px 3px #2B303A inset;
}
html, body { html, body {
height: 100%; height: 100%;
width: 100%; width: 100%;
margin: 0px; margin: 0px;
padding: 0px;
background-color: #2B303A; background-color: #2B303A;
} }
</style> </style>

View File

@ -0,0 +1,93 @@
<!-- https://loading.io/css/ -->
<!-- CC0 Creative Commons License -->
<template>
<div class="lds-roller"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
</template>
<style scoped>
.lds-roller {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-roller div {
animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
transform-origin: 40px 40px;
}
.lds-roller div:after {
content: " ";
display: block;
position: absolute;
width: 7px;
height: 7px;
border-radius: 50%;
background: #fff;
margin: -4px 0 0 -4px;
}
.lds-roller div:nth-child(1) {
animation-delay: -0.036s;
}
.lds-roller div:nth-child(1):after {
top: 63px;
left: 63px;
}
.lds-roller div:nth-child(2) {
animation-delay: -0.072s;
}
.lds-roller div:nth-child(2):after {
top: 68px;
left: 56px;
}
.lds-roller div:nth-child(3) {
animation-delay: -0.108s;
}
.lds-roller div:nth-child(3):after {
top: 71px;
left: 48px;
}
.lds-roller div:nth-child(4) {
animation-delay: -0.144s;
}
.lds-roller div:nth-child(4):after {
top: 72px;
left: 40px;
}
.lds-roller div:nth-child(5) {
animation-delay: -0.18s;
}
.lds-roller div:nth-child(5):after {
top: 71px;
left: 32px;
}
.lds-roller div:nth-child(6) {
animation-delay: -0.216s;
}
.lds-roller div:nth-child(6):after {
top: 68px;
left: 24px;
}
.lds-roller div:nth-child(7) {
animation-delay: -0.252s;
}
.lds-roller div:nth-child(7):after {
top: 63px;
left: 17px;
}
.lds-roller div:nth-child(8) {
animation-delay: -0.288s;
}
.lds-roller div:nth-child(8):after {
top: 56px;
left: 12px;
}
@keyframes lds-roller {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@ -1,6 +1,10 @@
<template> <template>
<div id="overseer-header"> <div id="overseer-header">
<router-link to="/">
<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="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>
<div style="float: right; margin: 10px 20px;"> <div style="float: right; margin: 10px 20px;">
<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 }}
@ -24,6 +28,15 @@ export default {
</script> </script>
<style scoped> <style scoped>
#overseer-websocket {
height: 18px;
width: 18px;
border-radius: 8px;
position: absolute;
left: 25px;
top: 20px;
z-index: -1;
}
#overseer-header { #overseer-header {
background-color: #C9582C; background-color: #C9582C;
width: 100%; width: 100%;
@ -31,4 +44,8 @@ export default {
position: fixed; position: fixed;
top: 0; top: 0;
} }
#overseer-header a {
color: inherit;
}
</style> </style>

View File

@ -1,48 +0,0 @@
<template>
<div id="overseer-body">
<div id="center">
<input @keyup.enter="onSubmit" id="overseer-search" placeholder="IP Address / Hostname" type="text">
</div>
</div>
</template>
<script>
export default {
name: 'OverseerHome',
methods: {
onSubmit(event){
alert(event.target.value)
}
}
}
</script>
<style scoped>
#overseer-body {
width: 100%;
height: 100%;
}
#center {
margin: 0px auto;
width: 1000px;
height: 100%;
background-color: #343A46;
/* background-color: #CAD2C5; */
box-shadow: 0 0 3px 3px #2B303A inset;
}
#overseer-search::placeholder{
text-align: center;
}
#overseer-search {
width: 400px;
height: 40px;
border-radius: 5px;
text-align: center;
margin-top: 400px;
font-size: 1rem;
padding: .3rem .8rem;
text-indent: 0px;
outline: none;
border: 0px solid;
}
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div id="overseer-notifications"> <div id="overseer-notifications">
<div class="overseer-notification" v-for="item in items" :key="item.id"> <div v-on:click="navigateToScan(item)" class="overseer-notification" v-for="item in items" :key="item.id">
<b>Target: </b> {{ item.target }} <br> <b>Target: </b> {{ item.target }} <br>
<b>Status: </b>{{ item.status }} <br> <b>Status: </b>{{ item.status }} <br>
<span v-if="'total_progress' in item"> <span v-if="'total_progress' in item">
@ -17,6 +17,11 @@ export default {
items() { items() {
return this.$store.state.notifications return this.$store.state.notifications
} }
},
methods: {
navigateToScan(item) {
this.$router.push({ path: `/scan/${item.target}/${item.id}` })
}
} }
} }
</script> </script>
@ -28,8 +33,10 @@ export default {
position: fixed; position: fixed;
transition: 0.5s; transition: 0.5s;
right: 0px; right: 0px;
top: 0px;
overflow: scroll; overflow: scroll;
margin-top: 60px; margin-top: 60px;
z-index: 1;
} }
.overseer-notification { .overseer-notification {
@ -40,5 +47,6 @@ export default {
text-align: start; text-align: start;
padding: 10px; padding: 10px;
margin: 10px; margin: 10px;
cursor: pointer;
} }
</style> </style>

View File

@ -1,8 +1,9 @@
import Vue from 'vue' import Vue from 'vue';
import App from './App.vue' import App from './App.vue';
import store from './store' import store from './store';
import router from './router';
import VueSocketIO from 'vue-socket.io';
import socketio from 'socket.io-client'; import socketio from 'socket.io-client';
import VueSocketIO from 'vue-socket.io'
Vue.use(new VueSocketIO({ Vue.use(new VueSocketIO({
debug: true, debug: true,
@ -16,9 +17,8 @@ Vue.use(new VueSocketIO({
Vue.config.productionTip = false Vue.config.productionTip = false
console.log(store)
new Vue({ new Vue({
render: h => h(App), render: h => h(App),
store, router,
store
}).$mount('#app') }).$mount('#app')

View File

@ -0,0 +1,38 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Scan from '../views/Scan.vue'
import NotFound from '../views/NotFound.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/scan/:target/:scan_id',
name: 'Scan',
component: Scan
},
{
path: '/scan/:target',
name: 'Scan',
component: Scan
},
{
path: '*',
name: 'NotFound',
component: NotFound
}
]
const router = new VueRouter({
mode: 'history',
base: '/',
routes
})
export default router

View File

@ -6,29 +6,58 @@ Vue.use(Vuex)
export default new Vuex.Store({ export default new Vuex.Store({
state: { state: {
ws_connected: false, ws_connected: false,
notifications: [] notifications: [],
scan_cache: {}
}, },
mutations: { getters: {
"SOCKET_connect"(state, status) { getScansByTarget(state, target){
state.ws_connected = true return state.scan_cache[target]
console.log("CONNECT: ", state, status);
},
"SOCKET_disconnect"(state, status) {
state.ws_connected = false
console.log("DISCONNECT: ", state, status);
},
"SOCKET_message"(state, message) {
let matchedItem = state.notifications.find(item => item.id == message.id);
if (matchedItem){
Object.keys(message).forEach(key => {
// matchedItem[key] = message[key]
Vue.set(matchedItem, key, message[key])
});
} else {
state.notifications.push(message);
}
} }
}, },
actions: { actions: {
getScansByTarget({ commit }, target){
fetch('/api/v1/scans/' + target)
.then(resp => resp.json())
.then(json => {
commit("SET_SCANS", { target, data: json.data });
});
},
},
mutations: {
"SET_SCANS"(state, { target, data }) {
console.log("SET_SCANS: ", target, data)
Vue.set(state.scan_cache, target, data)
},
"SOCKET_connect"(state) {
state.ws_connected = true
},
"SOCKET_disconnect"(state) {
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

@ -0,0 +1,45 @@
<template>
<input @keyup.enter="onSubmit" id="overseer-search" placeholder="IP Address / Hostname" type="text">
</template>
<script>
export default {
name: 'OverseerHome',
methods: {
onSubmit(event){
fetch('/api/v1/scans', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ "target": event.target.value })
}).then(resp => resp.json()).then(json => {
// TODO: HANDLE POTENTIAL ERROR
console.log("RESPONSE: ", json);
this.$router.push({ path: `/scan/${event.target.value}` })
}).catch(err => {
// TODO: HANDLE ERROR
console.log("ERROR: ", err);
});
}
}
}
</script>
<style scoped>
#overseer-search::placeholder{
text-align: center;
}
#overseer-search {
width: 400px;
height: 40px;
border-radius: 5px;
text-align: center;
margin-top: 400px;
font-size: 1rem;
padding: .3rem .8rem;
text-indent: 0px;
outline: none;
border: 0px solid;
}
</style>

View File

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

View File

@ -0,0 +1,161 @@
<template>
<div id="overseer-scan" style="padding: 50px;">
<h1 style="font-size: 2.5em; margin: 0px;">{{ $route.params.target }}</h1>
<div id="results">
<div v-if="getRequestedScan == '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>
</template>
<script>
import Loading from '../components/Loading.vue'
export default {
name: 'Scan',
components: {
Loading,
},
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,
}
},
dynamicProtocolStyle(proto) {
return {
"backgroundColor": proto == "TCP" ? "#E7E6FF" : "#D5EFFF",
}
},
normalizeDate(dateISO) {
let parsedDate = new Date(dateISO);
return parsedDate.toDateString() + " " + parsedDate.toLocaleTimeString()
}
},
computed: {
getRequestedScan() {
let scanCache = this.$store.state.scan_cache;
let target = this.$route.params.target;
let scanID = this.$route.params.scan_id;
if (!scanCache[target])
return "LOADING"
if (scanID)
return scanCache[target].find(item => item.id == scanID);
else
return scanCache[target][0] || "NO_RESULTS";
}
},
created(){
this.$store.dispatch("getScansByTarget", this.$route.params.target);
}
}
</script>
<style scoped>
#overseer-scan {
text-align: left;
}
#results {
width: 500px;
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 {
display: flex;
margin: 10px 43px;
width: 80px;
}
li span {
font-weight: 700;
}
ul {
display: flex;
column-count: 3;
flex-wrap: wrap;
flex-direction: row;
list-style-type: none;
padding: 0px;
}
</style>

View File

@ -1256,6 +1256,13 @@
dependencies: dependencies:
"@vue/cli-shared-utils" "^4.5.11" "@vue/cli-shared-utils" "^4.5.11"
"@vue/cli-plugin-router@~4.5.0":
version "4.5.12"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-router/-/cli-plugin-router-4.5.12.tgz#977c4b2b694cc03e9ef816112a5d58923493d0ac"
integrity sha512-DYNz5AA3W7Ewt3aaiOLGdYFt4MX4w/HTEtep+kPzP9S9tAknzyoIJXkaYzhwu8ArpEYwhWgtuCcDZ8hR6++DbA==
dependencies:
"@vue/cli-shared-utils" "^4.5.12"
"@vue/cli-plugin-vuex@^4.5.11": "@vue/cli-plugin-vuex@^4.5.11":
version "4.5.11" version "4.5.11"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.11.tgz#f6f619bcfb66c86cc45340d73152844635e548bd" resolved "https://registry.yarnpkg.com/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.11.tgz#f6f619bcfb66c86cc45340d73152844635e548bd"
@ -1342,6 +1349,24 @@
semver "^6.1.0" semver "^6.1.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
"@vue/cli-shared-utils@^4.5.12":
version "4.5.12"
resolved "https://registry.yarnpkg.com/@vue/cli-shared-utils/-/cli-shared-utils-4.5.12.tgz#0e0693d488336d284ffa658ff33b1ea22927d065"
integrity sha512-qnIQPJ4XckMoqYh9fJ0Y91QKMIb4Hiibrm9+k4E15QHpk5RaokuOpf10SsOr2NLPCXSWsHOLo3hduZSwHPGY/Q==
dependencies:
"@hapi/joi" "^15.0.1"
chalk "^2.4.2"
execa "^1.0.0"
launch-editor "^2.2.1"
lru-cache "^5.1.1"
node-ipc "^9.1.1"
open "^6.3.0"
ora "^3.4.0"
read-pkg "^5.1.1"
request "^2.88.2"
semver "^6.1.0"
strip-ansi "^6.0.0"
"@vue/component-compiler-utils@^3.1.0", "@vue/component-compiler-utils@^3.1.2": "@vue/component-compiler-utils@^3.1.0", "@vue/component-compiler-utils@^3.1.2":
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.2.0.tgz#8f85182ceed28e9b3c75313de669f83166d11e5d" resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.2.0.tgz#8f85182ceed28e9b3c75313de669f83166d11e5d"