feat: frontend

This commit is contained in:
Chris Milne 2026-05-08 16:45:33 +01:00
parent 875c0cc258
commit 4a7b66b481
7 changed files with 847 additions and 0 deletions

View file

@ -0,0 +1,556 @@
class Auth {
constructor() {
this.url = "http://localhost:8000"
this.settings = {
authority: "https://sso.sr2.uk/realms/sr2",
client_id: "chris-dev",
redirect_uri: this.url + "/signin-callback.html",
response_type: "code",
scope: "openid email profile",
response_mode: "fragment",
}
this.user_manager = new oidc.UserManager(this.settings)
this.get_user().then(() => {
window.is_authenticated = Boolean(this.user);
ui.update_auth_button()
})
}
async get_user() {
this.user = await this.user_manager.getUser()
}
async login() {
try {
this.user = await this.user_manager.signinPopup();
window.is_authenticated = Boolean(auth.user);
} catch (error) {
console.error(error);
}
}
async logout() {
try {
this.user = await this.user_manager.signoutPopup();
window.is_authenticated = Boolean(auth.user);
} catch (error) {
console.error(error);
}
}
}
class UI {
constructor() {
this.auth_button = document.getElementById("auth_button");
this.blocked_domains = document.getElementById("blocked_domains")
this.timer_status = document.getElementById("timer_status")
this.overall_status = document.getElementById("overall_status")
this.last_update_complete_time = document.getElementById("last_update_complete_time")
this.last_update_duration = document.getElementById("last_update_duration")
this.manual_update_btn = document.getElementById("manual_update_btn")
this.start_timer_btn = document.getElementById("start_timer_btn")
this.stop_timer_btn = document.getElementById("stop_timer_btn")
this.domain_search_btn = document.getElementById("domain_search_btn")
this.false_positive_load_btn = document.getElementById("false_positive_load_btn")
this.event_container = document.getElementById("events_container")
this.create_listeners()
}
create_listeners() {
this.auth_button.addEventListener("click", this.onclick_auth_button)
this.manual_update_btn.addEventListener("click", this.onclick_manual_update_btn)
this.start_timer_btn.addEventListener("click", this.onclick_start_timer_btn)
this.stop_timer_btn.addEventListener("click", this.onclick_stop_timer_btn)
this.domain_search_btn.addEventListener("click", this.onclick_domain_search_btn)
this.false_positive_load_btn.addEventListener("click", this.onclick_false_positive_load_btn)
this.event_container.addEventListener("click", this.onclick_event_button)
document.getElementById("modal_button").addEventListener("click", this.onclick_modal_button)
}
open_modal(title, message) {
document.getElementById("modal_title").innerText = title;
document.getElementById("modal_message").innerText = message;
document.getElementById("modal_backdrop").style.display = "flex";
}
set_loading(button_id, loading) {
const button = document.getElementById(button_id);
button.disabled = loading;
if (loading) {
button.dataset.original_text = button.innerText;
button.innerText = "Loading...";
} else {
button.innerText = button.dataset.original_text;
}
}
sync_timer_buttons() {
this.start_timer_btn.disabled = timer_running;
this.stop_timer_btn.disabled = !timer_running;
}
render_search_results(results) {
const container = document.getElementById("domain_search_results");
const result_count = Object.keys(results).length;
if (!result_count) {
container.innerHTML = `<div class="search-result">No results found.</div>`;
return;
}
container.innerHTML = "";
for (const [key, value] of Object.entries(results)) {
const row = document.createElement("div");
row.className = "search-result";
row.innerHTML = `
<div class="row">
<div class="col-8">
${key}
</div>
<div class="col-4 ${ui.event_status_class(value)}">
${value}
</div>
</div>
`;
container.appendChild(row);
}
}
onclick_auth_button() {
if (is_authenticated) {
auth.logout().then(() => {
ui.update_auth_button()
})
} else {
auth.login().then(() => {
ui.update_auth_button()
})
}
ui.update_auth_button()
}
update_auth_button() {
const button = document.getElementById("auth_button");
if (is_authenticated) {
button.innerText = "Logged In";
button.className = "button default-dm success";
} else {
button.innerText = "Logged Out";
button.className = "button default-dm secondary";
}
}
event_status_class(value) {
switch (value.toUpperCase()) {
case "ALLOWED":
return "status-good";
case "IGNORED":
return "status-warning";
default:
return "status-bad";
}
}
update_metrics(metrics){
this.blocked_domains.innerHTML = metrics.blocked_domain_count;
let date
let pretty_length
if(!metrics.last_update_complete_time){
date = "No update since last restart"
pretty_length = ""
} else {
date = new Date(metrics.last_update_complete_time * 1000).toLocaleString();
pretty_length = `${Math.round(metrics.last_update_length)} seconds`
}
this.last_update_complete_time.innerHTML = date;
this.last_update_duration.innerHTML = pretty_length;
let timer_value = ""
switch (metrics.TIMER_STATE) {
case 0:
timer_value = "RUNNING"
window.timer_running = true;
break;
case 1:
timer_value = "STOPPING"
window.timer_running = true;
break;
case 2:
timer_value = "STOPPED"
window.timer_running = false;
break;
}
this.timer_status.innerHTML = timer_value;
this.timer_status.className = timer_running ? "status-good" : "status-warning";
this.sync_timer_buttons()
let overall_status = ""
switch (metrics.MISP_STATE) {
case 0:
overall_status = "IDLE"
break;
case 1:
overall_status = "FETCHING"
break;
case 2:
overall_status = "UPDATING"
break;
case 3:
overall_status = "RELOADING"
break;
case 4:
overall_status = "ERROR"
break;
}
this.overall_status.innerHTML = overall_status;
}
render_false_positive_controls(domain_data) {
document.getElementById("false_positive_actions").style.display = "block";
const always_allow_btn = document.getElementById("always_allow_btn");
always_allow_btn.innerText = domain_data.always_allow ? "Enabled" : "Disabled";
always_allow_btn.className = domain_data.always_allow ? "button default-dm success" : "button default-dm secondary";
always_allow_btn.dataset.domain = domain_data.domain;
always_allow_btn.dataset.always_allow = domain_data.always_allow;
always_allow_btn.addEventListener("click", ui.onclick_always_allow_btn)
ui.event_container.innerHTML = "";
domain_data.events.forEach(event => {
const button = document.createElement("button");
button.className = `button event-btn ${event.ignored ? "default-dm warning" : "default-dm danger" }`;
button.innerText = event.id;
button.dataset.id = event.id;
button.dataset.ignored = event.ignored
button.dataset.domain = domain_data.domain;
ui.event_container.appendChild(button);
});
}
async onclick_manual_update_btn() {
ui.set_loading("manual_update_btn", true);
try {
const data = await api.manual_update()
if(data.state==="Starting"){
ui.open_modal(
"Update Triggered",
"Manual update process successfully started."
);
} else {
throw new Error()
}
} catch (error) {
ui.open_modal(
"Update Failed",
"Failed to trigger update process."
);
} finally {
ui.set_loading("manual_update_btn", false);
}
}
async onclick_start_timer_btn() {
ui.set_loading("start_timer_btn", true);
try {
const data = await api.start_timer()
api.update_metrics()
} catch (error) {
console.error(error);
} finally {
ui.set_loading("start_timer_btn", false);
}
}
async onclick_stop_timer_btn() {
ui.set_loading("stop_timer_btn", true);
try {
const data = await api.stop_timer()
api.update_metrics()
} catch (error) {
console.error(error);
} finally {
ui.set_loading("stop_timer_btn", false);
}
}
async onclick_domain_search_btn(){
ui.set_loading("domain_search_btn", true);
try {
const data = await api.domain_search()
ui.render_search_results(data)
} catch (error) {
console.error(error);
} finally {
ui.set_loading("domain_search_btn", false);
}
}
async load_false_positive_controls(){
ui.set_loading("false_positive_load_btn", true);
try {
const data = await api.load_domain_fp()
const parsed_data= {
domain: data.domain,
always_allow: data.always_allowed,
events: []
}
data.events.forEach(event => {
parsed_data.events.push({"id": event, "ignored": data.ignored_events.includes(event)});
})
ui.render_false_positive_controls(parsed_data)
} catch (error) {
console.error(error);
} finally {
ui.set_loading("false_positive_load_btn", false);
}
}
async onclick_false_positive_load_btn(){
await ui.load_false_positive_controls()
}
async onclick_event_button(event){
if (event.target === event.currentTarget) return;
const event_id = event.target.dataset.id;
const domain = event.target.dataset.domain;
const ignored = event.target.dataset.ignored === "true";
try {
if(ignored){
await api.reinstate_event(domain, event_id)
} else {
await api.ignore_event(domain, event_id)
}
await ui.load_false_positive_controls()
} catch (error) {
console.error(error);
}
}
async onclick_always_allow_btn(event){
const domain = event.target.dataset.domain;
const always_allowed = event.target.dataset.always_allow === "true";
try {
await api.change_always_allow(domain, always_allowed)
await ui.load_false_positive_controls()
} catch (error) {
console.error(error);
}
}
onclick_modal_button() {
document.getElementById("modal_backdrop").style.display = "none";
}
}
class API {
constructor() {
const self = this
this.update_metrics()
this.metrics_timer = setInterval(self.update_metrics, 5000)
}
update_metrics() {
axios.get(`misp/dashboard_metrics`)
.then(response => {
ui.update_metrics(response.data);
})
.catch(error => {
console.log(error)
})
}
async manual_update(published_timestamp = null){
const user = await auth.get_user()
return axios.put(
`misp/manual_update`,
{
"published_timestamp": published_timestamp
},
{
headers: {
"Authorization": `Bearer ${user?.access_token}`
}
}
)
.then(response => {
return response.data
})
.catch(error => {
console.log(error)
})
}
async start_timer(){
const user = await auth.get_user()
return axios.put(
`control/start_timer`,
{},
{
headers: {
"Authorization": `Bearer ${user?.access_token}`
}
}
)
.then(response => {
return response.data
})
.catch(error => {
console.log(error)
})
}
async stop_timer(){
const user = await auth.get_user()
return axios.put(
`control/stop_timer`,
{},
{
headers: {
"Authorization": `Bearer ${user?.access_token}`
}
}
)
.then(response => {
return response.data
})
.catch(error => {
console.log(error)
})
}
async domain_search(){
const user = await auth.get_user()
const search_term = document.getElementById("domain_search_input").value
return axios.get(
`misp/domain/search?domain=${search_term}`,
{},
{
headers: {
"Authorization": `Bearer ${user?.access_token}`
}
}
)
.then(response => {
return response.data
})
.catch(error => {
console.log(error)
})
}
async load_domain_fp(){
const user = await auth.get_user()
const search_term = document.getElementById("false_positive_domain").value
return axios.get(
`/misp/domain/details/${search_term}`,
{},
{
headers: {
"Authorization": `Bearer ${user?.access_token}`
}
}
)
.then(response => {
return response.data
})
.catch(error => {
console.log(error)
})
}
async ignore_event(domain, event_id){
const user = await auth.get_user()
return axios.patch(
`/misp/domain/events/${domain}/ignore?event=${event_id}`,
{},
{
headers: {
"Authorization": `Bearer ${user?.access_token}`
}
}
)
.then(response => {
return response.data
})
.catch(error => {
console.log(error)
})
}
async reinstate_event(domain, event_id){
const user = await auth.get_user()
return axios.patch(
`/misp/domain/events/${domain}/reinstate?event=${event_id}`,
{},
{
headers: {
"Authorization": `Bearer ${user?.access_token}`
}
}
)
.then(response => {
return response.data
})
.catch(error => {
console.log(error)
})
}
async change_always_allow(domain, currently_allowed){
const user = await auth.get_user()
return axios.patch(
`/misp/domain/always_allowed/${domain}?allow=${!currently_allowed}`,
{},
{
headers: {
"Authorization": `Bearer ${user?.access_token}`
}
}
)
.then(response => {
return response.data
})
.catch(error => {
console.log(error)
})
}
}
window.onload = () => {
window.ui = new UI()
window.auth = new Auth()
window.api = new API()
window.timer_running = false;
ui.sync_timer_buttons();
}

View file

@ -0,0 +1,120 @@
body {
margin: 0;
padding: 24px;
font-family: Arial, sans-serif;
}
.title {
font-size: 48px;
}
#auth_button {
max-height: 2.5rem;
}
.card {
border: 1px solid #333;
border-radius: 8px;
padding: 18px;
margin-bottom: 20px;
background-color: light-dark(#eee, #171717);
}
.card h2 {
margin-top: 0;
}
.label {
display: block;
margin-bottom: 6px;
font-size: 14px;
}
.input {
width: 100%;
padding: 10px;
border: 1px solid #444;
border-radius: 4px;
box-sizing: border-box;
}
.button {
padding: 10px 14px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.status-good {
color: #4ade80;
font-weight: bold;
}
.status-bad {
color: #f87171;
font-weight: bold;
}
.status-warning {
color: #facc15;
font-weight: bold;
}
.summary-row {
margin-bottom: 12px;
}
.search-results {
margin-top: 12px;
max-height: 400px;
overflow-y: auto;
border: 1px solid #333;
}
.search-result {
padding: 10px;
border-bottom: 1px solid #333;
}
.event-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.event-btn {
min-width: 80px;
}
.top-bar {
margin-bottom: 20px;
}
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.65);
display: none;
justify-content: center;
align-items: center;
}
.modal {
width: 400px;
background: #1c1c1c;
border: 1px solid #444;
border-radius: 8px;
padding: 24px;
}
.modal-footer {
margin-top: 20px;
text-align: right;
}

105
src/static/index.html Normal file
View file

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Title</title>
<link rel="stylesheet" href="/libraries/luxgrid_combined.min.css"/>
<link rel="stylesheet" href="/assets/index/styles.css"/>
</head>
<body>
<div class="container">
<div class="row top-bar">
<div class="col-2 justify-start">Logo</div>
<div class="col-8 text-center title">Title</div>
<div class="col-2 justify-end">
<button id="auth_button" class="button default-dm success">Logged In</button>
</div>
</div>
<div class="card">
<h2>System Status</h2>
<div class="row">
<div class="col-3 summary-row">
<div class="label">Total Blocked Domains</div>
<div id="blocked_domains"></div>
</div>
<div class="col-3 summary-row">
<div class="label">Timer Status</div>
<div id="timer_status" class="status-warning"></div>
</div>
<div class="col-3 summary-row">
<div class="label">Overall Status</div>
<div id="overall_status" class="status-good"></div>
</div>
<div class="col-3 summary-row">
<div class="label">Last Update</div>
<div id="last_update_complete_time"></div>
<div id="last_update_duration"></div>
</div>
</div>
</div>
<div class="card">
<h2>Actions</h2>
<div class="row">
<div class="col-4">
<button id="manual_update_btn" class="button default-dm primary w-100">Manual Update</button>
</div>
<div class="col-4">
<button id="start_timer_btn" class="button default-dm success w-100" disabled>Start Timer</button>
</div>
<div class="col-4">
<button id="stop_timer_btn" class="button default-dm danger w-100">Stop Timer</button>
</div>
</div>
</div>
<div class="card">
<h2>Domain Search</h2>
<label class="label">Search</label>
<input id="domain_search_input" class="input" type="text" placeholder="partial domain"/>
<div style="margin-top:12px;">
<button id="domain_search_btn" class="button default-dm primary">Search</button>
</div>
<div id="domain_search_results" class="search-results"></div>
</div>
<div class="card">
<h2>False Positive Handling</h2>
<div class="row">
<div class="col-8">
<label class="label">Domain</label>
<input id="false_positive_domain" class="input" type="text" placeholder="domain to manage"/>
</div>
<div class="col-4">
<label class="label">&nbsp;</label>
<button id="false_positive_load_btn" class="button default-dm primary w-100">Load</button>
</div>
</div>
<div id="false_positive_actions" style="display:none; margin-top:20px;">
<div class="row align-center">
<div class="col-4">
<div class="label">Always Allow</div>
</div>
<div class="col-8">
<button id="always_allow_btn" class="button default-dm secondary">Disabled</button>
</div>
</div>
<div style="margin-top:20px;">
<div class="label">Events</div>
<div id="events_container" class="event-grid"></div>
</div>
</div>
</div>
</div>
<div id="modal_backdrop" class="modal-backdrop">
<div class="modal">
<h3 id="modal_title">Status</h3>
<div id="modal_message"></div>
<div class="modal-footer">
<button id="modal_button" class="button default-dm primary">Close</button>
</div>
</div>
</div>
<script src="/libraries/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client-ts/3.3.0/browser/oidc-client-ts.min.js" integrity="sha512-c2vT43K5Ap/b44ZVGj+uRqqM/RQFtqludEYk8ztxzAPWcPJaKPSGpJRcbo1c2/PdszmNUvPqOzWERCKqT+P1Xg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="/assets/index/scripts.js" type="module"></script>
</body>
</html>

5
src/static/libraries/axios.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
body,button,input,select,textarea{color:light-dark(black,var(--dm-text-colour))}.col,[class*=col-]{padding-left:15px;padding-right:15px;box-sizing:border-box;min-width:8.33%}:root{color-scheme:light dark;--dm-text-colour:#e0e0e0;--primary-bg:#007bff;--secondary-bg:#6c757d;--success-bg:#28a745;--danger-bg:#dc3545;--warning-bg:#ffc107;--info-bg:#17a2b8;--light-bg:#f8f9fa;--dark-bg:#343a40;--light-bg-text:#212529;--dark-bg-text:white}.danger,.info,.light,.primary,.secondary,.success,.warning{color:var(--light-bg-text)}body{background-color:light-dark(white,#121212)}button{background-color:light-dark(#f0f0f0,#333);border-color:light-dark(white,#555);border-radius:2px}button:hover{background-color:light-dark(#e5e5e5,#555);border-color:light-dark(white,#666)}input,select,textarea{background-color:light-dark(#fafafa,#1e1e1e);border-style:solid;border-width:1px;border-color:light-dark(black,#333)}input:focus,select:focus,textarea:focus{border-color:light-dark(black,#036);border-radius:2px;outline:solid 2px}h1,h2,h3,h4,h5,h6{color:light-dark(black,#fff)}.default{background-color:var(--base-bg-colour)}.default-dm{background-color:light-dark(var(--base-bg-colour),oklch(from var(--base-bg-colour) clamp(0,l + .22,1) clamp(0,c - .02,1) h))}.lighten{background-color:oklch(from var(--base-bg-colour) clamp(0,l + .27,1) c h)}.darken{background-color:oklch(from var(--base-bg-colour) clamp(0,l - .09,1) c h)}.subtle{background-color:rgba(from var(--base-bg-colour) r g b / .6)}.primary{--base-bg-colour:var(--primary-bg)}.secondary{--base-bg-colour:var(--secondary-bg)}.success{--base-bg-colour:var(--success-bg)}.danger{--base-bg-colour:var(--danger-bg)}.warning{--base-bg-colour:var(--warning-bg)}.info{--base-bg-colour:var(--info-bg)}.light{--base-bg-colour:var(--light-bg)}.dark{--base-bg-colour:var(--dark-bg);color:var(--dark-bg-text)}.container{width:100%;margin-left:auto;margin-right:auto;padding-left:15px;padding-right:15px;box-sizing:border-box}.row{display:flex;margin-bottom:5px;flex-wrap:wrap}.col{flex:1;max-width:100%}.col-1{width:8.33%}.col-2{width:16.66%}.col-3,.w-25{width:25%}.col-4{width:33.33%}.col-5{width:41.66%}.col-6,.w-50{width:50%}.col-7{width:58.33%}.col-8{width:66.66%}.col-9,.w-75{width:75%}.col-10{width:83.33%}.col-11{width:91.66%}.col-12,.w-100{width:100%}@media (min-width:576px){.col-sm-1{width:8.33%}.col-sm-2{width:16.66%}.col-sm-3{width:25%}.col-sm-4{width:33.33%}.col-sm-5{width:41.66%}.col-sm-6{width:50%}.col-sm-7{width:58.33%}.col-sm-8{width:66.66%}.col-sm-9{width:75%}.col-sm-10{width:83.33%}.col-sm-11{width:91.66%}.col-sm-12{width:100%}}@media (min-width:768px){.col-md-1{width:8.33%}.col-md-2{width:16.66%}.col-md-3{width:25%}.col-md-4{width:33.33%}.col-md-5{width:41.66%}.col-md-6{width:50%}.col-md-7{width:58.33%}.col-md-8{width:66.66%}.col-md-9{width:75%}.col-md-10{width:83.33%}.col-md-11{width:91.66%}.col-md-12{width:100%}}@media (min-width:992px){.col-lg-1{width:8.33%}.col-lg-2{width:16.66%}.col-lg-3{width:25%}.col-lg-4{width:33.33%}.col-lg-5{width:41.66%}.col-lg-6{width:50%}.col-lg-7{width:58.33%}.col-lg-8{width:66.66%}.col-lg-9{width:75%}.col-lg-10{width:83.33%}.col-lg-11{width:91.66%}.col-lg-12{width:100%}}@media (min-width:1200px){.col-xl-1{width:8.33%}.col-xl-2{width:16.66%}.col-xl-3{width:25%}.col-xl-4{width:33.33%}.col-xl-5{width:41.66%}.col-xl-6{width:50%}.col-xl-7{width:58.33%}.col-xl-8{width:66.66%}.col-xl-9{width:75%}.col-xl-10{width:83.33%}.col-xl-11{width:91.66%}.col-xl-12{width:100%}}.axis-vert{flex-direction:column}.axis-horz{flex-direction:row}.justify-start{display:flex;justify-content:flex-start}.justify-end{display:flex;justify-content:flex-end}.justify-center{display:flex;justify-content:center}.justify-between{display:flex;justify-content:space-between}.justify-around{display:flex;justify-content:space-around}.justify-evenly{display:flex;justify-content:space-evenly}.align-start{display:flex;align-items:flex-start}.align-end{display:flex;align-items:flex-end}.align-center{display:flex;align-items:center}.h-25{height:25%}.h-50{height:50%}.h-75{height:75%}.h-100{height:100%}.vh-25{height:25vh}.vh-50{height:50vh}.vh-75{height:75vh}.vh-100{height:100vh}.vw-25{width:25vw}.vw-50{width:50vw}.vw-75{width:75vw}.vw-100{width:100vw}.text-center{text-align:center}.text-left{text-align:start}.text-right{text-align:end}

View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client-ts/3.3.0/browser/oidc-client-ts.min.js"></script>
<script>
const url = "http://localhost:8000"
const settings = {
authority: "https://sso.sr2.uk/realms/sr2",
client_id: "chris-dev",
redirect_uri: url + "/",
response_type: "code",
scope: "openid email profile",
response_mode: "fragment",
}
const user_manager = new oidc.UserManager(settings)
user_manager.signinPopupCallback()
.catch(error => {
console.error(error);
});
</script>
</body>
</html>

View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client-ts/3.3.0/browser/oidc-client-ts.min.js"></script>
<script>
const url = "http://localhost:8000"
const settings = {
authority: "https://sso.sr2.uk/realms/sr2",
client_id: "chris-dev",
redirect_uri: url + "/",
response_type: "code",
scope: "openid email profile",
response_mode: "fragment",
}
const user_manager = new oidc.UserManager(settings)
user_manager.signoutPopupCallback()
.catch(error => {
console.error(error);
});
</script>
</body>
</html>