added graph, changed layout

This commit is contained in:
Stefan Etringer 2025-05-23 10:37:53 +00:00
parent 97c77506c8
commit 41b0d8e355
11 changed files with 498 additions and 239 deletions

85
main.go
View File

@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"embed"
"os" "os"
"strings" "strings"
"time" "time"
@ -15,7 +16,9 @@ import (
"strconv" "strconv"
"sync" "sync"
"syscall" "syscall"
// "io"
"golang.org/x/net/html"
"gontrol/src/logger" "gontrol/src/logger"
"gontrol/src/randomname" "gontrol/src/randomname"
api "gontrol/src/server/api" api "gontrol/src/server/api"
@ -33,6 +36,9 @@ var (
db *sql.DB db *sql.DB
) )
//go:embed static/*
var staticFiles embed.FS
type Config struct { type Config struct {
@ -127,7 +133,7 @@ func listAgents(w http.ResponseWriter, r *http.Request) {
} }
} }
if strings.Contains(r.Header.Get("Accept"), "application") { if strings.Contains(r.Header.Get("Accept"), "json") {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
jsonData, err := json.Marshal(agents) jsonData, err := json.Marshal(agents)
if err != nil { if err != nil {
@ -174,12 +180,12 @@ func getAgentIds(w http.ResponseWriter, r *http.Request) {
} }
func logsHandler(w http.ResponseWriter, r *http.Request) { func logsHandler(w http.ResponseWriter, r *http.Request) {
// Warning this bit me in the nose: var count is []string, but // Warning this bit me in the nose: var countStr is []string, but
// variable = count[0] is casted to int automatically // variable = countStr[0] is casted to int automatically
// when the string is a number. Jesus Christ, this is odd behavior! // when the string is a number. Jesus Christ, this is odd behavior!
levels := r.URL.Query()["level"] levels := r.URL.Query()["level"]
countStr := r.URL.Query()["limit"] countStr := r.URL.Query()["limit"]
var limit int = 10 var limit int = 128
if len(countStr) > 0 { if len(countStr) > 0 {
parsedCount, err := strconv.Atoi(countStr[0]) parsedCount, err := strconv.Atoi(countStr[0])
if err == nil { if err == nil {
@ -209,6 +215,75 @@ func logsHandler(w http.ResponseWriter, r *http.Request) {
renderTemplate(w, "templates/partials/logs_partial.html", logs) renderTemplate(w, "templates/partials/logs_partial.html", logs)
} }
func proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
agentIP := r.URL.Query().Get("ip") // e.g., 10.0.0.42
if agentIP == "" {
http.Error(w, "Missing 'ip' parameter", http.StatusBadRequest)
return
}
// Construct the URL to proxy to
agentURL := "http://" + agentIP + ":8080"
// Send request to agent server
resp, err := http.Get(agentURL)
if err != nil {
http.Error(w, "Failed to reach agent: "+err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
// Parse the HTML from the agent's response
doc, err := html.Parse(resp.Body)
if err != nil {
http.Error(w, "Failed to parse agent response: "+err.Error(), http.StatusInternalServerError)
return
}
// Extract all <link> elements for stylesheets
var stylesheets []string
var extractStylesheets func(*html.Node)
extractStylesheets = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "link" {
for _, attr := range n.Attr {
if attr.Key == "rel" && attr.Val == "stylesheet" {
for _, attr := range n.Attr {
if attr.Key == "href" {
stylesheets = append(stylesheets, attr.Val)
}
}
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
extractStylesheets(c)
}
}
extractStylesheets(doc)
// Return the HTML and inject the stylesheets in the <head> section
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
// Inject the stylesheets into the <head> section of your page
fmt.Fprintf(w, "<html><head>")
for _, stylesheet := range stylesheets {
// Make sure the stylesheet is loaded properly (absolute URLs or proxy it)
fmt.Fprintf(w, `<link rel="stylesheet" href="%s">`, stylesheet)
}
fmt.Fprintf(w, "</head><body>")
// Now, serve the HTML content of the agent web app (or an iframe)
// Output the rest of the HTML (including the agent's content inside iframe)
fmt.Fprintf(w, `
<iframe src="%s" width="100%" height="800px" style="border:none;">
Your browser does not support iframes.
</iframe>
`, agentURL)
fmt.Fprintf(w, "</body></html>")
}
func main() { func main() {
@ -233,6 +308,8 @@ func main() {
webMux.HandleFunc("/agents/{agentId}", agentsHandler) webMux.HandleFunc("/agents/{agentId}", agentsHandler)
webMux.HandleFunc("/logs", logsHandler) webMux.HandleFunc("/logs", logsHandler)
webMux.HandleFunc("/logs/{level}", logsHandler) webMux.HandleFunc("/logs/{level}", logsHandler)
webMux.Handle("/static/", http.FileServer(http.FS(staticFiles)))
webMux.HandleFunc("/proxyAgent", proxyAgentHandler)
db = database.InitDB (cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name) db = database.InitDB (cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name)
defer db.Close() defer db.Close()

View File

@ -107,9 +107,9 @@ func GetAgents(db *sql.DB) ([]Agent, error) {
} }
func GetAgent(db *sql.DB, w http.ResponseWriter, r *http.Request, agentId string) (Agent, error) { func GetAgent(db *sql.DB, w http.ResponseWriter, r *http.Request, agentId string) (Agent, error) {
query := "Select agentId, agentName, agentType, initialContact, lastContact from agents where agentId = ?" query := "Select agentId, agentName, agentType, IPv4Address, initialContact, lastContact from agents where agentId = ?"
var agent Agent var agent Agent
err := db.QueryRow(query, agentId).Scan(&agent.AgentID, &agent.AgentName, &agent.AgentType, &agent.InitialContact, &agent.LastContact) err := db.QueryRow(query, agentId).Scan(&agent.AgentID, &agent.AgentName, &agent.AgentType,&agent.IPv4Address, &agent.InitialContact, &agent.LastContact)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
http.Error(w, "Agent not found", http.StatusNotFound) http.Error(w, "Agent not found", http.StatusNotFound)
return Agent{} , err return Agent{} , err

View File

@ -221,7 +221,7 @@ func (wsh webSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
break break
} }
log.Printf("Message from agent %s: %s", agentName, message) log.Printf("Message from agent %s: %s", agentName, message)
logger.InsertLog(logger.Info, fmt.Sprintf("Message from agent %s: %s", agentName, message)) logger.InsertLog(logger.Debug, fmt.Sprintf("Message from agent %s: %s", agentName, message))
if ch, ok := responseChannels.Load(agentName); ok { if ch, ok := responseChannels.Load(agentName); ok {
responseChan := ch.(chan string) responseChan := ch.(chan string)
@ -235,67 +235,6 @@ type Message struct {
Payload string `json:"payload"` Payload string `json:"payload"`
} }
// var executeCommand http.HandlerFunc = func(w http.ResponseWriter, r *http.Request){
// err := r.ParseForm()
// if err != nil {
// http.Error(w, "Invalid form data", http.StatusBadRequest)
// logger.InsertLog(logger.Info, "Invalid form data")
// return
// }
// agentName := r.FormValue("agentName")
// command := r.FormValue("command")
// agentSocketsMutex.Lock()
// conn, ok := agentSockets[agentName]
// agentSocketsMutex.Unlock()
// if !ok {
// http.Error(w, "Agent not connected", http.StatusNotFound)
// logger.InsertLog(logger.Info, "Agent not connected")
// return
// }
// responseChan := make(chan string, 1)
// responseChannels.Store(agentName, responseChan)
// defer responseChannels.Delete(agentName)
// message := Message {
// Type: "command",
// Payload: command,
// }
// messageBytes, _ := json.Marshal(message)
// err = conn.WriteMessage(websocket.TextMessage, messageBytes)
// if err != nil {
// http.Error(w, "Failed to send command to the agent", http.StatusInternalServerError)
// logger.InsertLog(logger.Error, "Failed to send command to the agent")
// return
// }
// select {
// case response := <-responseChan:
// var parsedResponse map[string]string
// if err := json.Unmarshal([]byte(response), &parsedResponse); err != nil {
// http.Error(w, "Failed to parse response", http.StatusInternalServerError)
// return
// }
// payload, ok := parsedResponse["payload"]
// if !ok {
// http.Error(w, "Invalid response structure", http.StatusInternalServerError)
// logger.InsertLog(logger.Error, "Invalid response structure")
// return
// }
// w.WriteHeader(http.StatusOK)
// w.Header().Set("Content-Type", "text/plain")
// w.Write([]byte(payload))
// case <- time.After(10 * time.Second):
// http.Error(w, "Agent response timed out", http.StatusGatewayTimeout)
// logger.InsertLog(logger.Info, "Agent response timed out")
// }
// }
var executeCommand http.HandlerFunc = func(w http.ResponseWriter, r *http.Request){ var executeCommand http.HandlerFunc = func(w http.ResponseWriter, r *http.Request){
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {

185
static/agents-graph.js Normal file
View File

@ -0,0 +1,185 @@
let agentData = [];
let isCyInitialized = false;
let cy;
function initializeCytoscape() {
if (isCyInitialized) return;
cy = cytoscape({
container: document.getElementById('cyto-graph'),
style: [
{
selector: 'node',
style: {
'background-color': '#007bff',
'label': 'data(name)', // Ensure the label uses the AgentName
'color': 'white',
'text-outline-width': 2,
'text-outline-color': '#333',
'width': '50px',
'height': '50px'
}
},
{
selector: 'edge',
style: {
'width': 3,
'line-color': '#ccc',
'target-arrow-color': '#ccc',
'target-arrow-shape': 'triangle'
}
}
],
layout: {
name: 'grid',
rows: 2
}
});
isCyInitialized = true; // Mark Cytoscape as initialized
}
// Load the graph after the page has fully loaded
document.addEventListener('DOMContentLoaded', function () {
console.log('DOMContentLoaded fired.');
initializeCytoscape();
loadGraphData();
});
// Load the graph after HTMX swap
document.body.addEventListener('htmx:afterSwap', function (event) {
console.log('htmx:afterSwap fired.');
if (event.target.id === 'agentList') {
initializeCytoscape();
loadGraphData();
}
});
async function updateGraph(agentData) {
if (!cy) {
console.error('Cytoscape is not initialized yet.');
return;
}
console.log('Updating graph with agent data:', agentData);
// Clear existing nodes and edges
cy.elements().remove();
// Add nodes for each agent with the AgentName as the label
agentData.forEach(agent => {
const id = agent.agentId;
const name = agent.agentName;
const status = agent.status;
if (id && name) {
let nodeColor = (status === 'Connected') ? '#28a745' : '#dc3545'; // Green for connected, Red for disconnected
cy.add({
group: 'nodes',
data: {
id: id,
name: name,
status: status,
type: agent.agentType,
ip: agent.IPv4Address
},
style: {
'background-color': nodeColor,
'label': name, // Display agent's name
'color': 'white',
'text-outline-width': 2,
'text-outline-color': '#333',
'width': '50px',
'height': '50px'
}
});
} else {
console.warn('Skipping agent with missing data:', agent);
}
});
// Define the target node (`g2` in your case)
const targetNode = 'g2';
// Ensure the target node (`g2`) exists, if not, create it
if (cy.getElementById(targetNode).length === 0) {
cy.add({
group: 'nodes',
data: {
id: targetNode,
name: 'g2',
status: 'Target',
type: 'Server',
ip: 'N/A'
},
style: {
'background-color': '#6c757d', // Gray for target node
'label': 'g2',
'color': 'white',
'text-outline-width': 2,
'text-outline-color': '#333',
'width': '50px',
'height': '50px'
}
});
}
// Connect each agent to the target node (`g2`)
agentData.forEach(agent => {
const id = agent.agentId;
if (id) {
cy.add({
group: 'edges',
data: {
source: id,
target: targetNode
}
});
} else {
console.warn('Skipping edge for agent with missing agentId:', agent);
}
});
// Force a layout update
cy.layout({
name: 'grid',
rows: 2
}).run();
}
async function fetchData() {
const url = "http://localhost:3333/agents";
try {
const response = await fetch(url, {headers: {Accept: 'application/json'}});
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const data = await response.json();
return data; // Return the fetched data
} catch (error) {
console.error(error.message);
return []; // Return an empty array on error
}
}
// Function to get agent data and update the graph
async function loadGraphData() {
console.log("Function loadGraphData()");
// Fetch agent data asynchronously
agentData = await fetchData();
// Check if the data is valid
console.log('Extracted agent data:', agentData);
// Only update the graph if agent data is available
if (agentData && agentData.length > 0) {
await updateGraph(agentData);
} else {
console.log('No agent data found or extracted.');
}
}

84
static/gontrol-helper.js Normal file
View File

@ -0,0 +1,84 @@
const checkboxState = new Map();
document.addEventListener('DOMContentLoaded', () => {
document.body.addEventListener('htmx:beforeSwap', function(event) {
if (event.target.id === "agentList") {
saveCheckboxState();
}
});
document.body.addEventListener('htmx:afterSwap', function(event) {
if (event.target.id === "agentList") {
restoreCheckboxState();
updateAgentDropdown();
}
});
});
function prepareAgentNames(event) {
const selected = Array.from(document.querySelectorAll('.agent-checkbox'))
.filter(cb => cb.checked)
.map(cb => cb.dataset.agentName);
const hiddenInput = document.getElementById('agentNamesInput');
if (selected.length > 0) {
document.getElementById('agentName').removeAttribute('name');
hiddenInput.value = selected.join(',');
} else {
document.getElementById('agentName').setAttribute('name', 'agentName');
hiddenInput.value = '';
}
}
function toggleAllCheckboxes() {
const checkboxes = document.querySelectorAll('input[name="agent-checkbox"]');
const allChecked = Array.from(checkboxes).every(checkbox => checkbox.checked);
checkboxes.forEach(checkbox => checkbox.checked = !allChecked);
}
function saveCheckboxState() {
document.querySelectorAll('.agent-checkbox').forEach((checkbox) => {
checkboxState.set(checkbox.dataset.agentName, checkbox.checked);
});
}
function restoreCheckboxState() {
document.querySelectorAll('.agent-checkbox').forEach((checkbox) => {
const state = checkboxState.get(checkbox.dataset.agentName);
if (state !== undefined) {
checkbox.checked = state;
}
});
}
function updateAgentDropdown() {
const select = document.getElementById("agentName");
const optionValues = Array.from(select.options).map(opt => opt.value);
const rows = document.querySelectorAll("#agentList tbody tr");
rows.forEach(row => {
const status = row.cells[4].textContent.trim();
const name = row.cells[1].textContent.trim();
if (status === "Connected") {
row.cells[4].innerHTML = '<span class="badge bg-success">Connected</span>';
const option = document.createElement("option");
if (!(optionValues.includes(name))) {
option.value = name;
option.textContent = name;
select.appendChild(option);
}
}
if (status === "Disconnected") {
row.cells[4].innerHTML = '<span class="badge bg-danger">Disconnected</span>';
const option = Array.from(select.options).find(opt => opt.value === name);
if(option) {
select.removeChild(option);
}
}
});
}

44
static/stylesheet.css Normal file
View File

@ -0,0 +1,44 @@
:root{
--grey-color: #1B2B34;
--error-color: #EC5f67;
--warning-color: #F99157;
--yellow-color: #FAC863;
--info-color: #99C794;
--teal-color: #5FB3B3;
--blue-color: #6699CC;
--debug-color: #C594C5;
--fatal-color: #AB7967;
}
#logs-container {
height: 300px; /* or any fixed height you prefer */
overflow-y: auto; /* enables vertical scroll when content overflows */
border: 1px solid #ccc; /* optional: for visual clarity */
padding: 10px; /* optional: spacing inside the container */
background-color: #f8f9fa; /* optional: subtle background for log readability */
}
.log-info, .log-warning, .log-error, .log-fatal, .log-debug{
font-family: "Lucida Console", Monaco, monospace;
font-size: 12px;
}
.log-info {
color: var(--info-color);
}
.log-warning {
color: var(--warning-color);
}
.log-error {
color: var(--error-color);
}
.log-fatal {
color: var(--fatal-color);
}
.log-debug {
color: var(--debug-color);
}
#graph-container {
margin: 0; padding: 0;
}

View File

@ -5,185 +5,108 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="static/stylesheet.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/htmx.org@1.9.12"></script> <script src="https://unpkg.com/htmx.org@1.9.12"></script>
<!-- <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> --> <!-- Include Cytoscape.js -->
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.23/dist/cytoscape.min.js"></script>
<script type="text/javascript" src="static/agents-graph.js"></script>
<script type="text/javascript" src="static/gontrol-helper.js"></script>
<title>g2: gommand & gontrol</title> <title>g2: gommand & gontrol</title>
<script>
const checkboxState = new Map();
document.addEventListener('DOMContentLoaded', () => {
document.body.addEventListener('htmx:beforeSwap', function(event) {
if (event.target.id === "agentList") {
saveCheckboxState();
}
});
document.body.addEventListener('htmx:afterSwap', function(event) {
if (event.target.id === "agentList") {
restoreCheckboxState();
updateAgentDropdown();
}
});
});
function prepareAgentNames(event) {
const selected = Array.from(document.querySelectorAll('.agent-checkbox'))
.filter(cb => cb.checked)
.map(cb => cb.dataset.agentName);
const hiddenInput = document.getElementById('agentNamesInput');
if (selected.length > 0) {
document.getElementById('agentName').removeAttribute('name');
hiddenInput.value = selected.join(',');
} else {
document.getElementById('agentName').setAttribute('name', 'agentName');
hiddenInput.value = '';
}
}
function saveCheckboxState() {
document.querySelectorAll('.agent-checkbox').forEach((checkbox) => {
checkboxState.set(checkbox.dataset.agentName, checkbox.checked);
});
}
function restoreCheckboxState() {
document.querySelectorAll('.agent-checkbox').forEach((checkbox) => {
const state = checkboxState.get(checkbox.dataset.agentName);
if (state !== undefined) {
checkbox.checked = state;
}
});
}
function updateAgentDropdown() {
const select = document.getElementById("agentName");
const optionValues = Array.from(select.options).map(opt => opt.value);
const rows = document.querySelectorAll("#agentList tbody tr");
rows.forEach(row => {
const status = row.cells[4].textContent.trim();
const name = row.cells[1].textContent.trim();
if (status === "Connected") {
row.cells[4].innerHTML = '<span class="badge bg-success">Connected</span>';
const option = document.createElement("option");
if (!(optionValues.includes(name))) {
option.value = name;
option.textContent = name;
select.appendChild(option);
}
}
if (status === "Disconnected") {
row.cells[4].innerHTML = '<span class="badge bg-danger">Disconnected</span>';
const option = Array.from(select.options).find(opt => opt.value === name);
if(option) {
select.removeChild(option);
}
}
});
}
</script>
<style>
:root{
--grey-color: #1B2B34;
--error-color: #EC5f67;
--warning-color: #F99157;
--yellow-color: #FAC863;
--info-color: #99C794;
--teal-color: #5FB3B3;
--blue-color: #6699CC;
--debug-color: #C594C5;
--fatal-color: #AB7967;
}
.log-info, .log-warning, .log-error, .log-fatal, .log-debug{
font-family: "Lucida Console", Monaco, monospace;
}
.log-info {
color: var(--info-color);
}
.log-warning {
color: var(--warning-color);
}
.log-error {
color: var(--error-color);
}
.log-fatal {
color: var(--fatal-color);
}
.log-debug {
color: var(--debug-color);
}
</style>
</head> </head>
<body> <body>
<div class="container"> <div class="container-fluid px-4 py-3">
<div class="row">
<div class="col">
<h2>Agents</h2>
<!-- Agent List --> <!-- Top Row: Graph + Logs (equal height) -->
<div id="agentList" hx-get="/agents" hx-trigger="load, every 2s" hx-swap="innerHTML"></div> <div class="row mb-3">
<!-- <div id="agentList" hx-get="/agents" hx-trigger="load" hx-swap="innerHTML"></div> --> <!-- Graph -->
<!-- Agent Commands --> <div class="col-md-6 d-flex flex-column">
<div id="agentCommands"> <h3>Agents</h3>
<h3>Command Execution</h3> <div class="flex-grow-1 border" id="cyto-graph" style="height: 100%; min-height: 320px;"></div>
<form hx-post="http://localhost:5555/executeCommand" hx-target="#commandOutput" hx-encoding="application/x-www-form-urlencoded" hx-swap="innerHTML" onsubmit="prepareAgentNames(event)"> </div>
<div class="mb-3">
<label for="agentName" class="form-label">Agent Name</label>
<!-- <select class="form-select" id="agentName" name="agentName" required> -->
<select id="agentName" class="form-select" name="agentName" hx-on="htmx:afterSwap:updateAgentDropdown">
<option value="" disabled selected>Select an Agent</option>
<!-- Dynamically populated with agent names -->
</select>
</div>
<div class="mb-3">
<label for="command" class="form-label">Command</label>
<input type="text" class="form-control" id="command" name="command" placeholder="Enter command" required>
</div>
<button type="submit" class="btn btn-primary">Execute</button>
<!-- Hidden checkbox form !-->
<input type="hidden" name="agentNames" id="agentNamesInput">
</form>
<pre id="commandOutput"></pre>
</div>
<!-- Agent Details --> <!-- Logs -->
<div class="col" id="agentDetails"> <div class="col-md-6 d-flex flex-column">
<h3>Details</h3>
<p>Select an agent to view details.</p>
</div>
<!-- Logs Section -->
<h3>Logs</h3> <h3>Logs</h3>
<form id="log-filter-form"
<form id="log-filter-form"
hx-get="/logs" hx-get="/logs"
hx-target="#logs-container" hx-target="#logs-container"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-trigger="change from:.log-filter, every 3s" hx-trigger="change from:.log-filter, load, every 5s"
hx-include="#log-filter-form"> hx-include="#log-filter-form">
<div class="btn-group mb-2" role="group" aria-label="Log filter">
<input type="checkbox" class="btn-check log-filter" id="info" name="level" value="info" checked>
<label class="btn btn-outline-primary" for="info">Info</label>
<label><input type="checkbox" class="log-filter" name="level" value="info" checked> Info</label> <input type="checkbox" class="btn-check log-filter" id="warning" name="level" value="warning">
<label><input type="checkbox" class="log-filter" name="level" value="warning"> Warning</label> <label class="btn btn-outline-primary" for="warning">Warning</label>
<label><input type="checkbox" class="log-filter" name="level" value="error"> Error</label>
<label><input type="checkbox" class="log-filter" name="level" value="debug"> Debug</label>
<label><input type="checkbox" class="log-filter" name="level" value="fatal"> Fatal</label>
</form> <input type="checkbox" class="btn-check log-filter" id="error" name="level" value="error">
<label class="btn btn-outline-primary" for="error">Error</label>
<div id="logs-container"> <input type="checkbox" class="btn-check log-filter" id="fatal" name="level" value="fatal">
<!-- Logs will load here --> <label class="btn btn-outline-primary" for="fatal">Fatal</label>
</div>
<input type="checkbox" class="btn-check log-filter" id="debug" name="level" value="debug">
<label class="btn btn-outline-primary" for="debug">Debug</label>
</div>
</form>
<div id="logs-container" class="flex-grow-1 border" style="overflow-y: auto; min-height: 320px;">
<!-- Logs will load here -->
</div> </div>
</div>
</div> </div>
<!-- Bottom Row: Agent List + Command Execution -->
<div class="row">
<!-- Agent List -->
<div class="col-md-6">
<div id="agentList"
hx-get="/agents"
hx-trigger="load, every 2s"
hx-swap="innerHTML">
</div>
</div>
<!-- Command Execution -->
<div class="col-md-6">
<div id="agentCommands">
<h5>Command Execution</h5>
<form hx-post="http://localhost:5555/executeCommand"
hx-target="#commandOutput"
hx-encoding="application/x-www-form-urlencoded"
hx-swap="innerHTML"
onsubmit="prepareAgentNames(event)">
<div class="mb-3">
<label for="agentName" class="form-label">Agent Name</label>
<select id="agentName" class="form-select" name="agentName"
hx-on="htmx:afterSwap:updateAgentDropdown">
<option value="" disabled selected>Select an Agent</option>
</select>
</div>
<div class="mb-3">
<label for="command" class="form-label">Command</label>
<input type="text" class="form-control" id="command" name="command" placeholder="Enter command" required>
</div>
<button type="submit" class="btn btn-primary">Execute</button>
<input type="hidden" name="agentNames" id="agentNamesInput">
</form>
<pre id="commandOutput"></pre>
</div>
</div>
</div>
<!-- Offcanvas for Agent Details -->
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel" data-bs-scroll="true">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvasRightLabel">Agent Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div id="offcanvas-body" class="offcanvas-body">
...
</div>
</div>
</div>
</body> </body>
</html> </html>

View File

@ -45,7 +45,7 @@
logs.forEarch(log => { logs.forEarch(log => {
const logElement = document.createElement('p'); const logElement = document.createElement('p');
logElement.innerHTML = `<strong>ts=${log.timestamp} level=${log.level}</strong> msg=${log.message}`; logElement.innerHTML = `<strong>level=${log.level}</strong> msg=${log.message}`;
if (log.level ==== 'INFO') { if (log.level ==== 'INFO') {
logElement.classList.add('log-info'); logElement.classList.add('log-info');

View File

@ -1,9 +1,11 @@
<div id="agent-detail"> <div id="agent-detail">
<h2>Agent Details</h2> <!-- <h2>Agent Details</h2> -->
<p>ID: {{.AgentID}}</p> <p>ID: {{.AgentID}}</p>
<p>Name: {{.AgentName}}</p> <p>Name: {{.AgentName}}</p>
<p>Type: {{.AgentType}}</p> <p>Type: {{.AgentType}}</p>
<p>IP Addr: {{.IPv4Address}}</p>
<p>Initial Contact: {{.InitialContact}}</p> <p>Initial Contact: {{.InitialContact}}</p>
<p>Last Contact: {{.LastContact}}</p> <p>Last Contact: {{.LastContact}}</p>
<button hx-get="/agents" hx-target="#agentList" hx-swap="innerHTML">Back to List</button> <!-- <button hx-get="/proxyAgent?ip={{.IPv4Address}}" hx-target="#agentConnect" hx-swap="innerHTML">Open</button> -->
<a href="http://{{.IPv4Address}}:8080" class="btn btn-info" target="_blank">Open</a>
</div> </div>

View File

@ -8,7 +8,8 @@
<!-- <th>Initial Contact</th> --> <!-- <th>Initial Contact</th> -->
<!-- <th>Last Contact</th> --> <!-- <th>Last Contact</th> -->
<th>Status</th> <th>Status</th>
<th>Actions</th> <!-- <th>Actions <input type="checkbox" class="select-agent-checkbox" onClick="toggleAllCheckboxes(this)"></th> -->
<th><button type="button" class="btn btn-primary btn-sm" onClick="toggleAllCheckboxes()">Toggle Agents</button></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -24,10 +25,14 @@
<!-- <td><span class="badge bg-danger">Disconnected</span></td> --> <!-- <td><span class="badge bg-danger">Disconnected</span></td> -->
<td>{{.Status}}</td> <td>{{.Status}}</td>
<td> <td>
<input type="checkbox" class="agent-checkbox" data-agent-name="{{.AgentName}}"> <button class="btn btn-danger btn-sm" hx-delete="/agents/{{.AgentID}}" hx-target="#agentList" hx-swap="innerHTML">Delete</button>
<button class="btn btn-warning" hx-get="/agents/{{.AgentID}}" hx-target="#agentDetails" hx-swap="innerHTML">View</button> <!-- <button class="btn btn-warning btn-sm" hx-get="/agents/{{.AgentID}}" hx-target="#agentDetails" hx-swap="innerHTML">View</button> -->
<button class="btn btn-danger" hx-delete="/agents/{{.AgentID}}" hx-target="#agentList" hx-swap="innerHTML">Delete</button> <button class="btn btn-warning btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasRight" aria-controls="offcanvasRight" hx-get="/agents/{{.AgentID}}" hx-target="#offcanvas-body">View</button>
<input type="checkbox" class="agent-checkbox" data-agent-name="{{.AgentName}}" name="agent-checkbox">
<!-- <input type="checkbox" class="agent-checkbox btn-check" data-agent-name="{{.AgentName}}" id="{{.AgentName}}"> -->
<!-- <label class="btn btn-outline-primary btn-sm" for="{{.AgentName}}">Select</label> -->
</td> </td>
</tr> </tr>
{{end}} {{end}}

View File

@ -1,5 +1,5 @@
{{range .}} {{range .}}
<div class="log-{{.Level}}"> <div class="log-{{.Level}}">
ts={{.Timestamp}} level={{.Level}}msg="{{.Message}}" ts={{.Timestamp}} level={{.Level}} msg="{{.Message}}"
</div> </div>
{{end}} {{end}}