Consistency, Routing, Progress, Styling
This commit is contained in:
parent
3d1e945fe1
commit
6d4cc4a11b
src
@ -25,11 +25,11 @@ def post_scans():
|
||||
if data is None or "target" not in data:
|
||||
return {"error": "Missing 'target'"}, 422
|
||||
|
||||
history_id = overseer.scan_manager.perform_scan(data["target"])
|
||||
if history_id is None:
|
||||
scan_history = overseer.scan_manager.perform_scan(data["target"])
|
||||
if scan_history is None:
|
||||
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"])
|
||||
@ -51,7 +51,7 @@ def get_scans_by_target(target):
|
||||
page, hostname=target
|
||||
)
|
||||
|
||||
return {"data": __normalize_scan_results(scan_results)}
|
||||
return {"data": __normalize_scan_results(scan_results, target)}
|
||||
|
||||
|
||||
@api.route("/search", methods=["GET"])
|
||||
@ -66,13 +66,15 @@ def get_scans(search):
|
||||
return "SEARCH PLACEHOLDER"
|
||||
|
||||
|
||||
def __normalize_scan_results(scan_results):
|
||||
def __normalize_scan_results(scan_results, target):
|
||||
return list(
|
||||
map(
|
||||
lambda x: {
|
||||
"results": x.results.split(","),
|
||||
"created_at": x.created_at,
|
||||
"id": x.id,
|
||||
"target": target,
|
||||
"status": x.status,
|
||||
"results": x.results.split(",") if x.results != "" else [],
|
||||
"created_at": x.created_at.isoformat(),
|
||||
"error": x.error,
|
||||
},
|
||||
scan_results,
|
||||
|
@ -11,6 +11,14 @@ def main_entry():
|
||||
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>")
|
||||
def static_resources(path):
|
||||
"""
|
||||
|
@ -17,18 +17,22 @@ class ScanManager:
|
||||
|
||||
def __broadcast_thread(self):
|
||||
while not self.pending_shutdown:
|
||||
time.sleep(5)
|
||||
time.sleep(1)
|
||||
if len(self.active_scans) == 0:
|
||||
continue
|
||||
|
||||
for scan in self.active_scans:
|
||||
# WebSocket progress
|
||||
total_progress = (scan.tcp_progress + scan.udp_progress) / 2
|
||||
results = scan.get_results()
|
||||
overseer.api.send_websocket_event(
|
||||
{
|
||||
"id": scan.history_id,
|
||||
"id": scan.scan_history.id,
|
||||
"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,
|
||||
"udp_progress": scan.udp_progress,
|
||||
"total_progress": round(total_progress),
|
||||
@ -38,25 +42,20 @@ class ScanManager:
|
||||
if scan.tcp_progress + scan.udp_progress != 200:
|
||||
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
|
||||
overseer.database.update_scan_result(
|
||||
scan.history_id, "COMPLETE", results=results
|
||||
scan_history = overseer.database.update_scan_result(
|
||||
scan.scan_history.id, "COMPLETE", results=results
|
||||
)
|
||||
|
||||
# WebSocket completion
|
||||
overseer.api.send_websocket_event(
|
||||
{
|
||||
"id": scan.history_id,
|
||||
"id": scan_history.id,
|
||||
"target": scan.target,
|
||||
"status": "COMPLETE",
|
||||
"status": scan_history.status,
|
||||
"results": results,
|
||||
"created_at": scan_history.created_at.isoformat(),
|
||||
"error": scan_history.error,
|
||||
}
|
||||
)
|
||||
|
||||
@ -72,9 +71,12 @@ class ScanManager:
|
||||
return list(
|
||||
map(
|
||||
lambda x: {
|
||||
"id": x.history_id,
|
||||
"id": x.scan_history.id,
|
||||
"target": x.target,
|
||||
"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,
|
||||
"udp_progress": x.udp_progress,
|
||||
"total_progress": round(
|
||||
@ -100,10 +102,10 @@ class ScanManager:
|
||||
"IN_PROGRESS", hostname=target
|
||||
)
|
||||
|
||||
new_scan = Scanner(target, scan_history.id)
|
||||
new_scan = Scanner(target, scan_history)
|
||||
new_scan.start()
|
||||
self.active_scans.append(new_scan)
|
||||
return scan_history.id
|
||||
return scan_history
|
||||
|
||||
def is_ip(self, target):
|
||||
try:
|
||||
@ -114,10 +116,10 @@ class ScanManager:
|
||||
|
||||
|
||||
class Scanner(Thread):
|
||||
def __init__(self, target, history_id):
|
||||
def __init__(self, target, scan_history):
|
||||
Thread.__init__(self)
|
||||
self.target = target
|
||||
self.history_id = history_id
|
||||
self.scan_history = scan_history
|
||||
|
||||
self.port_count = 1000
|
||||
|
||||
@ -181,6 +183,14 @@ class Scanner(Thread):
|
||||
udp_thread.join()
|
||||
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):
|
||||
for port in range(1, self.port_count):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
1
src/overseer/static/css/app.a50f799c.css
Normal file
1
src/overseer/static/css/app.a50f799c.css
Normal 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}
|
@ -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
2
src/overseer/static/js/app.e5dd211e.js
Normal file
2
src/overseer/static/js/app.e5dd211e.js
Normal file
File diff suppressed because one or more lines are too long
1
src/overseer/static/js/app.e5dd211e.js.map
Normal file
1
src/overseer/static/js/app.e5dd211e.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
21
src/overseer/static/js/chunk-vendors.6130e7bc.js → src/overseer/static/js/chunk-vendors.cf069257.js
21
src/overseer/static/js/chunk-vendors.6130e7bc.js → src/overseer/static/js/chunk-vendors.cf069257.js
File diff suppressed because one or more lines are too long
1
src/overseer/static/js/chunk-vendors.cf069257.js.map
Normal file
1
src/overseer/static/js/chunk-vendors.cf069257.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
@ -18,6 +18,7 @@
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
|
@ -2,19 +2,21 @@
|
||||
<div id="overseer-app">
|
||||
<OverseerHeader/>
|
||||
<OverseerNotifications/>
|
||||
<OverseerHome/>
|
||||
<div id="overseer-body">
|
||||
<div id="overseer-body-center">
|
||||
<router-view/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OverseerHome from './components/OverseerHome.vue'
|
||||
import OverseerHeader from './components/OverseerHeader.vue'
|
||||
import OverseerNotifications from './components/OverseerNotifications.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
OverseerHome,
|
||||
OverseerHeader,
|
||||
OverseerNotifications
|
||||
}
|
||||
@ -23,18 +25,38 @@ export default {
|
||||
|
||||
<style>
|
||||
#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;
|
||||
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 {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
background-color: #2B303A;
|
||||
}
|
||||
</style>
|
||||
|
93
src/overseer_client/src/components/Loading.vue
Normal file
93
src/overseer_client/src/components/Loading.vue
Normal 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>
|
@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<div id="overseer-header">
|
||||
<div style="float: left; margin: 8px 20px; font-weight: 700; font-size: 2em;">Overseer</div>
|
||||
<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>
|
||||
</router-link>
|
||||
<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;">
|
||||
{{ notificationCount }}
|
||||
@ -24,6 +28,15 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#overseer-websocket {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
border-radius: 8px;
|
||||
position: absolute;
|
||||
left: 25px;
|
||||
top: 20px;
|
||||
z-index: -1;
|
||||
}
|
||||
#overseer-header {
|
||||
background-color: #C9582C;
|
||||
width: 100%;
|
||||
@ -31,4 +44,8 @@ export default {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#overseer-header a {
|
||||
color: inherit;
|
||||
}
|
||||
</style>
|
||||
|
@ -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>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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>Status: </b>{{ item.status }} <br>
|
||||
<span v-if="'total_progress' in item">
|
||||
@ -17,6 +17,11 @@ export default {
|
||||
items() {
|
||||
return this.$store.state.notifications
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
navigateToScan(item) {
|
||||
this.$router.push({ path: `/scan/${item.target}/${item.id}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -28,8 +33,10 @@ export default {
|
||||
position: fixed;
|
||||
transition: 0.5s;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
overflow: scroll;
|
||||
margin-top: 60px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.overseer-notification {
|
||||
@ -40,5 +47,6 @@ export default {
|
||||
text-align: start;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,8 +1,9 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import store from './store'
|
||||
import Vue from 'vue';
|
||||
import App from './App.vue';
|
||||
import store from './store';
|
||||
import router from './router';
|
||||
import VueSocketIO from 'vue-socket.io';
|
||||
import socketio from 'socket.io-client';
|
||||
import VueSocketIO from 'vue-socket.io'
|
||||
|
||||
Vue.use(new VueSocketIO({
|
||||
debug: true,
|
||||
@ -16,9 +17,8 @@ Vue.use(new VueSocketIO({
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
console.log(store)
|
||||
|
||||
new Vue({
|
||||
render: h => h(App),
|
||||
store,
|
||||
router,
|
||||
store
|
||||
}).$mount('#app')
|
||||
|
38
src/overseer_client/src/router/index.js
Normal file
38
src/overseer_client/src/router/index.js
Normal 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
|
@ -6,29 +6,58 @@ Vue.use(Vuex)
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
ws_connected: false,
|
||||
notifications: []
|
||||
notifications: [],
|
||||
scan_cache: {}
|
||||
},
|
||||
mutations: {
|
||||
"SOCKET_connect"(state, status) {
|
||||
state.ws_connected = true
|
||||
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);
|
||||
}
|
||||
getters: {
|
||||
getScansByTarget(state, target){
|
||||
return state.scan_cache[target]
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
45
src/overseer_client/src/views/Home.vue
Normal file
45
src/overseer_client/src/views/Home.vue
Normal 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>
|
6
src/overseer_client/src/views/NotFound.vue
Normal file
6
src/overseer_client/src/views/NotFound.vue
Normal file
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<div style="padding-top: 300px;">
|
||||
<h1>Error</h1>
|
||||
<h4>404 Page Not Found</h4>
|
||||
</div>
|
||||
</template>
|
161
src/overseer_client/src/views/Scan.vue
Normal file
161
src/overseer_client/src/views/Scan.vue
Normal 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>
|
@ -1256,6 +1256,13 @@
|
||||
dependencies:
|
||||
"@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":
|
||||
version "4.5.11"
|
||||
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"
|
||||
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":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.2.0.tgz#8f85182ceed28e9b3c75313de669f83166d11e5d"
|
||||
|
Loading…
x
Reference in New Issue
Block a user