sync: Auto-sync from DESKTOP-0O8A1RL at 2026-04-02 19:20:43

Synced files:
- Session logs updated
- Latest context and credentials
- Command/directive updates

Machine: DESKTOP-0O8A1RL
Timestamp: 2026-04-02 19:20:43

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 19:20:43 -07:00
parent 6e4ebc2db9
commit bff7d9dbbf
8 changed files with 981 additions and 27 deletions

View File

@@ -0,0 +1,559 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Security Incident Report - Ace Portables - 31 March 2026</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
:root {
--primary: #1a1a2e;
--accent: #e87a1e;
--accent-light: #f5a623;
--text: #2c2c2c;
--text-light: #666;
--border: #e0e0e0;
--bg-light: #f8f9fa;
--bg-green: #e8f5e9;
--green: #2e7d32;
--red: #c62828;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: var(--text);
line-height: 1.6;
background: #fff;
}
.page {
max-width: 850px;
margin: 0 auto;
padding: 0;
}
/* Header */
.header {
background: #fff;
color: var(--text);
padding: 35px 50px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 3px solid var(--accent);
}
.header-left img {
height: 60px;
width: auto;
}
.header-left .report-type {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 2.5px;
color: var(--accent);
margin-top: 10px;
}
.header-right {
text-align: right;
font-size: 13px;
line-height: 1.8;
color: var(--text-light);
}
.header-right strong {
color: var(--text);
}
/* Status Banner */
.status-banner {
background: var(--bg-green);
border-left: 5px solid var(--green);
padding: 20px 50px;
display: flex;
align-items: center;
gap: 15px;
}
.status-icon {
width: 48px;
height: 48px;
background: var(--green);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.status-icon svg {
width: 28px;
height: 28px;
fill: #fff;
}
.status-text h3 {
font-size: 18px;
font-weight: 700;
color: var(--green);
margin-bottom: 2px;
}
.status-text p {
font-size: 13px;
color: var(--text-light);
}
/* Content */
.content {
padding: 40px 50px;
}
.section {
margin-bottom: 35px;
}
.section-title {
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--accent);
border-bottom: 2px solid var(--accent);
padding-bottom: 8px;
margin-bottom: 18px;
}
p, li {
font-size: 14px;
color: var(--text);
margin-bottom: 10px;
}
ul {
padding-left: 20px;
}
li {
margin-bottom: 6px;
}
/* Info Grid */
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
}
.info-item {
padding: 12px 18px;
border-bottom: 1px solid var(--border);
border-right: 1px solid var(--border);
}
.info-item:nth-child(even) {
border-right: none;
}
.info-item:nth-last-child(-n+2) {
border-bottom: none;
}
.info-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-light);
margin-bottom: 3px;
}
.info-value {
font-size: 14px;
font-weight: 500;
color: var(--text);
word-break: break-all;
}
.info-value.mono {
font-family: 'Courier New', monospace;
font-size: 12px;
}
.info-item.full-width {
grid-column: 1 / -1;
border-right: none;
}
/* Machine Status Table */
.machine-table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
font-size: 14px;
}
.machine-table thead {
background: var(--primary);
color: #fff;
}
.machine-table th {
padding: 12px 18px;
text-align: left;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
.machine-table td {
padding: 12px 18px;
border-bottom: 1px solid var(--border);
}
.machine-table tr:last-child td {
border-bottom: none;
}
.machine-table tr:nth-child(even) {
background: var(--bg-light);
}
.badge {
display: inline-block;
padding: 3px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.badge-clean {
background: var(--bg-green);
color: var(--green);
}
.badge-managed {
background: #e3f2fd;
color: #1565c0;
}
.badge-deleted {
background: #fce4ec;
color: var(--red);
}
/* Timeline */
.timeline {
position: relative;
padding-left: 30px;
}
.timeline::before {
content: '';
position: absolute;
left: 8px;
top: 5px;
bottom: 5px;
width: 2px;
background: var(--border);
}
.timeline-item {
position: relative;
margin-bottom: 20px;
}
.timeline-item::before {
content: '';
position: absolute;
left: -26px;
top: 6px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--accent);
border: 2px solid #fff;
box-shadow: 0 0 0 2px var(--accent);
}
.timeline-date {
font-size: 12px;
font-weight: 600;
color: var(--accent);
margin-bottom: 4px;
}
.timeline-text {
font-size: 14px;
color: var(--text);
}
/* Footer */
.footer {
background: var(--primary);
color: #ccc;
padding: 30px 50px;
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-top: 20px;
}
.footer-left h4 {
color: #fff;
font-size: 16px;
margin-bottom: 8px;
}
.footer-left h4 span {
color: var(--accent);
}
.footer-left p {
font-size: 13px;
color: #aaa;
margin-bottom: 3px;
}
.footer-right {
text-align: right;
font-size: 12px;
color: #888;
max-width: 300px;
}
.footer-right p {
font-size: 12px;
color: #888;
margin-bottom: 3px;
}
.divider {
border: none;
border-top: 1px solid var(--border);
margin: 25px 0;
}
.page-break {
page-break-before: always;
break-before: page;
}
@media print {
body { background: #fff; }
.page { max-width: 100%; }
.header, .footer { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.section { break-inside: avoid; }
.timeline { break-inside: avoid; }
.info-grid { break-inside: avoid; }
.machine-table { break-inside: avoid; }
.page-break { page-break-before: always; break-before: page; }
}
</style>
</head>
<body>
<div class="page">
<!-- Header -->
<div class="header">
<div class="header-left">
<img src="logo-light.png" alt="Arizona ComputerGuru">
<div class="report-type">Security Incident Report</div>
</div>
<div class="header-right">
<strong>Report Reference:</strong> ACE-SEC-2026-0331<br>
<strong>Date:</strong> 31 March 2026<br>
<strong>Prepared for:</strong> Ace Portables
</div>
</div>
<!-- Status Banner -->
<div class="status-banner">
<div class="status-icon">
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</div>
<div class="status-text">
<h3>ALL SYSTEMS VERIFIED CLEAN</h3>
<p>Both workstations have been scanned, verified, and are actively protected by enterprise-grade endpoint security. No active threats detected.</p>
</div>
</div>
<!-- Content -->
<div class="content">
<!-- Executive Summary -->
<div class="section">
<div class="section-title">Executive Summary</div>
<p>
Ace Portables contacted AZ Computer Guru LLC after their financial institution requested verification that company workstations were free of malware. Upon investigation, we determined that the previously installed antivirus software (McAfee) had silently expired, leaving the machines unprotected.
</p>
<p>
We removed the expired McAfee installation and deployed <strong>Bitdefender GravityZone</strong>, an enterprise-grade Endpoint Detection and Response (EDR) platform, across both company workstations. During the initial security scan, Bitdefender detected and automatically deleted a malicious browser extension containing a Trojan on one machine. Both machines have been fully scanned and are confirmed clean with no active threats.
</p>
</div>
<!-- Incident Timeline -->
<div class="section">
<div class="section-title">Incident Timeline</div>
<div class="timeline">
<div class="timeline-item">
<div class="timeline-date">Prior to Engagement</div>
<div class="timeline-text">McAfee antivirus subscription silently expired, leaving workstations without active endpoint protection.</div>
</div>
<div class="timeline-item">
<div class="timeline-date">Engagement Initiated</div>
<div class="timeline-text">Ace Portables contacted AZ Computer Guru LLC at the request of their bank to verify workstation security.</div>
</div>
<div class="timeline-item">
<div class="timeline-date">Remediation</div>
<div class="timeline-text">Expired McAfee software removed. Bitdefender GravityZone EDR deployed on both workstations (DESKTOP-DV7I10S, DESKTOP-U317856).</div>
</div>
<div class="timeline-item">
<div class="timeline-date">25 March 2026, 11:15</div>
<div class="timeline-text">Bitdefender detected and automatically deleted a Trojan (Trojan.GenericKD.77292516) within a malicious Microsoft Edge browser extension on one workstation.</div>
</div>
<div class="timeline-item">
<div class="timeline-date">31 March 2026</div>
<div class="timeline-text">Full scans completed on both machines. Both verified clean. This report issued.</div>
</div>
</div>
</div>
<!-- Threat Details -->
<div class="section page-break">
<div class="section-title">Threat Details</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Threat Classification</div>
<div class="info-value">Trojan.GenericKD.77292516</div>
</div>
<div class="info-item">
<div class="info-label">Threat Type</div>
<div class="info-value">Malware (Trojan)</div>
</div>
<div class="info-item">
<div class="info-label">Detection Date</div>
<div class="info-value">25 March 2026, 11:15</div>
</div>
<div class="info-item">
<div class="info-label">Action Taken</div>
<div class="info-value"><span class="badge badge-deleted">Automatically Deleted</span></div>
</div>
<div class="info-item">
<div class="info-label">Affected Component</div>
<div class="info-value">Microsoft Edge Browser Extension (background.js)</div>
</div>
<div class="info-item">
<div class="info-label">Extension ID</div>
<div class="info-value mono">cfacibcmkcdppnkgennk...blmp</div>
</div>
<div class="info-item full-width">
<div class="info-label">File SHA-256 Hash</div>
<div class="info-value mono">B3F83B5EC4CFED5D93561B86B5A124FA88D2EA35491011D32CCDA3E385C036E1</div>
</div>
</div>
</div>
<!-- Machines Scanned -->
<div class="section">
<div class="section-title">Workstation Scan Results</div>
<p>Both Ace Portables workstations were enrolled in Bitdefender GravityZone and scanned. Current status as of 31 March 2026:</p>
<br>
<table class="machine-table">
<thead>
<tr>
<th>Machine Name</th>
<th>Type</th>
<th>Management</th>
<th>Security Status</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>DESKTOP-DV7I10S</strong></td>
<td>Physical Machine</td>
<td><span class="badge badge-managed">Managed</span></td>
<td><span class="badge badge-clean">No Issues</span></td>
</tr>
<tr>
<td><strong>DESKTOP-U317856</strong></td>
<td>Physical Machine</td>
<td><span class="badge badge-managed">Managed</span></td>
<td><span class="badge badge-clean">No Issues</span></td>
</tr>
</tbody>
</table>
</div>
<!-- Remediation Steps -->
<div class="section">
<div class="section-title">Remediation Actions Taken</div>
<ul>
<li><strong>Removed expired antivirus software</strong> &mdash; McAfee, which had silently expired, was fully uninstalled from both workstations.</li>
<li><strong>Deployed enterprise endpoint protection</strong> &mdash; Bitdefender GravityZone EDR was installed and configured on both machines, providing real-time threat monitoring, behavioral analysis, and automated response.</li>
<li><strong>Malicious extension deleted</strong> &mdash; The Trojan-infected browser extension was automatically detected and removed by Bitdefender during the initial scan.</li>
<li><strong>Extension blocked globally</strong> &mdash; The malicious extension has been added to our managed blocklist, preventing it from being installed on any endpoint under our management.</li>
<li><strong>Full system scans completed</strong> &mdash; Comprehensive antimalware scans were run on both workstations. Both returned clean results with no further threats detected.</li>
<li><strong>Password reset recommended</strong> &mdash; The affected user was advised to change passwords for all accounts accessed via the browser, prioritising financial and email accounts.</li>
</ul>
</div>
<!-- Ongoing Protection -->
<div class="section">
<div class="section-title">Ongoing Protection</div>
<p>Both Ace Portables workstations are now continuously protected by Bitdefender GravityZone, which provides:</p>
<ul>
<li><strong>Real-time file system protection</strong> &mdash; On-access scanning of all files as they are opened, created, or modified.</li>
<li><strong>Advanced Threat Control</strong> &mdash; Behavioral monitoring that detects suspicious process activity in real time.</li>
<li><strong>Network Attack Defense</strong> &mdash; Protection against network-based exploits and lateral movement attempts.</li>
<li><strong>Web Threat Protection</strong> &mdash; Blocks access to known malicious, phishing, and fraudulent websites.</li>
<li><strong>Anti-Exploit Technology</strong> &mdash; Detects and prevents exploitation of software vulnerabilities.</li>
<li><strong>Centralised Management</strong> &mdash; All endpoints are monitored and managed through the GravityZone console by AZ Computer Guru LLC, ensuring policies and definitions remain current.</li>
</ul>
</div>
<hr class="divider">
<!-- Conclusion -->
<div class="section">
<p>
Both Ace Portables workstations have been verified clean and are now actively protected by enterprise-grade endpoint security. The previously unprotected state caused by the expired McAfee subscription has been fully resolved. The detected Trojan was automatically removed before any confirmed data exfiltration occurred, and preventative measures are in place to block future threats.
</p>
<p>
Should the bank require any additional information, technical logs, or further clarification, please do not hesitate to contact us using the details below.
</p>
</div>
</div>
<!-- Footer -->
<div class="footer">
<div class="footer-left">
<h4>Arizona <span>Computer</span>Guru LLC</h4>
<p>7437 E. 22nd St, Tucson, AZ 85710</p>
<p>Phone: (520) 304-8300</p>
<p>Web: azcomputerguru.com</p>
</div>
<div class="footer-right">
<p>This report is confidential and intended solely for the use of Ace Portables and their financial institution.</p>
<br>
<p>Report Ref: ACE-SEC-2026-0331</p>
<p>Date Issued: 31 March 2026</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,106 @@
# Security Incident Report - Malware Detection and Remediation
**Prepared by:** AZ Computer Guru LLC
**Prepared for:** Ace Portables
**Date:** 31 March 2026
**Report Reference:** ACE-SEC-2026-0331
---
## Executive Summary
On 25 March 2026, our endpoint protection platform detected and automatically removed a malicious browser extension from a workstation belonging to Ace Portables. The threat was identified, quarantined, and deleted without user intervention. Additional preventative measures have been implemented across the managed environment to prevent recurrence.
---
## Incident Details
| Field | Detail |
|-------|--------|
| **Date of Detection** | 25 March 2026, 11:15 |
| **Affected Machine User** | John |
| **Threat Classification** | Trojan.GenericKD.77292516 |
| **Threat Type** | Malware (Trojan) |
| **Affected File** | `background.js` (browser extension component) |
| **File Location** | Microsoft Edge browser extension directory |
| **Extension ID** | cfacibcmkcdppnkgennkfaepplpkblmp |
| **File SHA256 Hash** | B3F83B5EC4CFED5D93561B86B5A124FA88D2EA35491011D32CCDA3E385C036E1 |
---
## Detection and Response
### Detection
The threat was identified by **Bitdefender GravityZone**, our enterprise endpoint detection and response (EDR) platform, during a scheduled on-demand scan task. The malicious file was a JavaScript component (`background.js`) operating within a Microsoft Edge browser extension.
### Automated Response
Bitdefender GravityZone automatically took the following action upon detection:
- **Action Taken:** File deleted
- **Detection Module:** Antimalware (On-Demand Scan)
- **Result:** Threat successfully removed from the system
### Additional Remediation Steps
The following manual remediation steps were performed by AZ Computer Guru LLC:
1. **Extension removal verified** - Confirmed the malicious browser extension was fully removed from Microsoft Edge, including all associated files and registry entries.
2. **Extension blocked at policy level** - The malicious extension (ID: `cfacibcmkcdppnkgennkfaepplpkblmp`) has been added to the GravityZone extension blocklist, preventing installation across all managed endpoints company-wide.
3. **Full system scan completed** - A comprehensive antimalware scan was conducted on the affected workstation to confirm no additional threats or residual malicious components remain.
4. **Browser data review** - Edge browser settings were reviewed and restored to safe defaults where necessary.
5. **Password reset recommended** - The affected user was advised to change passwords for all accounts accessed via the browser as a precautionary measure, with priority given to financial and email accounts.
---
## Current System Status
**The affected workstation is confirmed CLEAN and free of malware.** Bitdefender GravityZone endpoint protection continues to actively monitor the system in real time with:
- Real-time file system protection (on-access scanning)
- Network attack defense
- Web threat protection
- Advanced anti-exploit technology
- Behavioral monitoring (Advanced Threat Control)
The GravityZone management console shows **no active threats** on the affected machine or any other Ace Portables endpoints.
---
## Preventative Measures Implemented
| Measure | Scope | Status |
|---------|-------|--------|
| Malicious extension added to blocklist | All managed client endpoints | Complete |
| Full system scan on affected workstation | Affected machine | Complete - Clean |
| User advised to reset browser passwords | Affected user | Advised |
| Ongoing real-time endpoint monitoring | All Ace Portables endpoints | Active |
---
## About Our Security Platform
AZ Computer Guru LLC utilises **Bitdefender GravityZone**, an enterprise-grade endpoint protection platform that provides:
- Multi-layered malware detection (signature, heuristic, behavioural, and machine learning)
- Real-time threat monitoring and automated response
- Centralised management and policy enforcement
- Regular definition updates and cloud-based threat intelligence
---
## Conclusion
The malicious browser extension was detected promptly by our automated security systems, removed before any confirmed data exfiltration occurred, and blocked from future installation. The affected workstation has been verified clean and continues to be actively protected. No further action is required at this time.
Should the bank require any additional information, technical logs, or clarification, please do not hesitate to contact us.
---
**AZ Computer Guru LLC**
Managed IT Services Provider
---
*This report is confidential and intended solely for the use of Ace Portables and their financial institution.*

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -113,7 +113,7 @@ export interface Command {
agent_id: string;
command_type: string;
command_text: string;
status: "pending" | "running" | "completed" | "failed";
status: "pending" | "running" | "completed" | "failed" | "cancelled";
exit_code: number | null;
stdout: string | null;
stderr: string | null;
@@ -219,6 +219,11 @@ export const commandsApi = {
api.post<Command>(`/api/agents/${agentId}/command`, command),
list: () => api.get<Command[]>("/api/commands"),
get: (id: string) => api.get<Command>(`/api/commands/${id}`),
cancelCommand: (id: string) =>
api.post<{ status: string; message: string }>(`/api/commands/${id}/cancel`),
deleteCommand: (id: string) => api.delete(`/api/commands/${id}`),
clearCommandHistory: () =>
api.delete<{ deleted: number; message: string }>("/api/commands"),
};
export const clientsApi = {

View File

@@ -1,6 +1,17 @@
import { useQuery } from "@tanstack/react-query";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Link, useParams, useNavigate } from "react-router-dom";
import { RefreshCw, CheckCircle, XCircle, Clock, Loader2, ArrowLeft, Terminal } from "lucide-react";
import {
RefreshCw,
CheckCircle,
XCircle,
Clock,
Loader2,
ArrowLeft,
Terminal,
Trash2,
Ban,
StopCircle,
} from "lucide-react";
import { commandsApi, Command } from "../api/client";
import { Card, CardContent } from "../components/Card";
import { Button } from "../components/Button";
@@ -28,6 +39,11 @@ function StatusBadge({ status }: { status: Command["status"] }) {
label: "Failed",
className: "bg-rose-500/10 text-rose-400 border-rose-500/30",
},
cancelled: {
icon: Ban,
label: "Cancelled",
className: "bg-amber-500/10 text-amber-500 border-amber-500/30",
},
};
const { icon: Icon, label, className, spin } = config[status] as {
@@ -62,12 +78,63 @@ function formatRelativeTime(dateString: string): string {
}
export function History() {
const queryClient = useQueryClient();
const { data: commands = [], isLoading, refetch } = useQuery({
queryKey: ["commands"],
queryFn: () => commandsApi.list().then((res) => res.data),
refetchInterval: 10000,
});
const cancelMutation = useMutation({
mutationFn: (id: string) => commandsApi.cancelCommand(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["commands"] });
},
onError: (error: Error) => {
alert(`Failed to cancel command: ${error.message}`);
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => commandsApi.deleteCommand(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["commands"] });
},
onError: (error: Error) => {
alert(`Failed to delete command: ${error.message}`);
},
});
const clearHistoryMutation = useMutation({
mutationFn: () => commandsApi.clearCommandHistory(),
onSuccess: (res) => {
queryClient.invalidateQueries({ queryKey: ["commands"] });
const data = res.data;
if (data.deleted === 0) {
alert("No finished commands to clear.");
}
},
onError: (error: Error) => {
alert(`Failed to clear history: ${error.message}`);
},
});
const handleClearHistory = () => {
const finishedCount = commands.filter(
(cmd) => cmd.status === "completed" || cmd.status === "failed" || cmd.status === "cancelled"
).length;
if (finishedCount === 0) {
alert("No finished commands to clear.");
return;
}
if (window.confirm(`Clear ${finishedCount} finished command(s) from history?`)) {
clearHistoryMutation.mutate();
}
};
return (
<div className="space-y-4">
{/* Header */}
@@ -76,15 +143,30 @@ export function History() {
<h1 className="text-2xl font-mono font-bold text-[var(--text-primary)]">History</h1>
<p className="text-[var(--text-muted)] text-sm">Command execution log</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => refetch()}
className="border-[var(--border-accent)] text-[var(--accent-cyan)] hover:bg-[var(--accent-cyan-muted)]"
>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
<div className="flex items-center gap-2">
<Button
variant="destructive"
size="sm"
onClick={handleClearHistory}
disabled={clearHistoryMutation.isPending}
>
{clearHistoryMutation.isPending ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Trash2 className="h-4 w-4 mr-2" />
)}
Clear History
</Button>
<Button
variant="outline"
size="sm"
onClick={() => refetch()}
className="border-[var(--border-accent)] text-[var(--accent-cyan)] hover:bg-[var(--accent-cyan-muted)]"
>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
</div>
</div>
{/* History List */}
@@ -103,12 +185,14 @@ export function History() {
) : (
<div className="divide-y divide-[var(--border-secondary)]">
{commands.map((cmd: Command) => (
<Link
<div
key={cmd.id}
to={`/history/${cmd.id}`}
className="flex items-center justify-between p-3 hover:bg-[rgba(6,182,212,0.05)] transition-colors group"
>
<div className="flex items-center gap-3 min-w-0 flex-1">
<Link
to={`/history/${cmd.id}`}
className="flex items-center gap-3 min-w-0 flex-1"
>
<StatusBadge status={cmd.status} />
<div className="min-w-0 flex-1">
<p className="font-mono text-sm text-[var(--text-primary)] truncate group-hover:text-[var(--accent-cyan)] transition-colors">
@@ -118,18 +202,49 @@ export function History() {
{cmd.command_type} | Agent: {cmd.agent_id.slice(0, 8)}...
</p>
</div>
</div>
<div className="text-right pl-4 shrink-0">
<p className="font-mono text-xs text-[var(--text-muted)]">
{formatRelativeTime(cmd.created_at)}
</p>
{cmd.exit_code !== null && (
<p className={`text-xs font-mono ${cmd.exit_code === 0 ? "text-emerald-500" : "text-rose-500"}`}>
exit: {cmd.exit_code}
</Link>
<div className="flex items-center gap-2 pl-4 shrink-0">
<div className="text-right">
<p className="font-mono text-xs text-[var(--text-muted)]">
{formatRelativeTime(cmd.created_at)}
</p>
)}
{cmd.exit_code !== null && (
<p className={`text-xs font-mono ${cmd.exit_code === 0 ? "text-emerald-500" : "text-rose-500"}`}>
exit: {cmd.exit_code}
</p>
)}
</div>
{/* Action buttons */}
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
{(cmd.status === "pending" || cmd.status === "running") && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
cancelMutation.mutate(cmd.id);
}}
disabled={cancelMutation.isPending}
className="p-1.5 rounded-md text-amber-400 hover:bg-amber-500/10 hover:text-amber-300 transition-colors"
title="Cancel command"
>
<StopCircle className="h-4 w-4" />
</button>
)}
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
deleteMutation.mutate(cmd.id);
}}
disabled={deleteMutation.isPending}
className="p-1.5 rounded-md text-rose-400 hover:bg-rose-500/10 hover:text-rose-300 transition-colors"
title="Delete command"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
</div>
</Link>
</div>
))}
</div>
)}
@@ -142,6 +257,7 @@ export function History() {
export function HistoryDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { data: commands = [], isLoading } = useQuery({
queryKey: ["commands"],
@@ -150,6 +266,27 @@ export function HistoryDetail() {
const command = commands.find((cmd: Command) => cmd.id === id);
const cancelMutation = useMutation({
mutationFn: (cmdId: string) => commandsApi.cancelCommand(cmdId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["commands"] });
},
onError: (error: Error) => {
alert(`Failed to cancel command: ${error.message}`);
},
});
const deleteMutation = useMutation({
mutationFn: (cmdId: string) => commandsApi.deleteCommand(cmdId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["commands"] });
navigate("/history");
},
onError: (error: Error) => {
alert(`Failed to delete command: ${error.message}`);
},
});
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[50vh]">
@@ -199,6 +336,33 @@ export function HistoryDetail() {
{formatDate(command.created_at)}
</p>
</div>
<div className="flex items-center gap-2">
{(command.status === "pending" || command.status === "running") && (
<Button
variant="outline"
size="sm"
onClick={() => cancelMutation.mutate(command.id)}
disabled={cancelMutation.isPending}
className="border-amber-500/30 text-amber-400 hover:bg-amber-500/10"
>
<StopCircle className="h-4 w-4 mr-2" />
Cancel
</Button>
)}
<Button
variant="destructive"
size="sm"
onClick={() => {
if (window.confirm("Delete this command?")) {
deleteMutation.mutate(command.id);
}
}}
disabled={deleteMutation.isPending}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
</div>
</div>
{/* Command Info */}

View File

@@ -160,3 +160,100 @@ pub async fn get_command(
Ok(Json(command))
}
/// Delete a single command by ID
/// Requires authentication.
pub async fn delete_command(
State(state): State<AppState>,
_user: AuthUser,
Path(command_id): Path<Uuid>,
) -> Result<StatusCode, (StatusCode, String)> {
let deleted = db::delete_command(&state.db, command_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
if deleted {
Ok(StatusCode::NO_CONTENT)
} else {
Err((StatusCode::NOT_FOUND, "Command not found".to_string()))
}
}
/// Cancel response payload
#[derive(Debug, Serialize)]
pub struct CancelCommandResponse {
pub status: String,
pub message: String,
}
/// Cancel a pending or running command
/// Requires authentication.
pub async fn cancel_command(
State(state): State<AppState>,
_user: AuthUser,
Path(command_id): Path<Uuid>,
) -> Result<Json<CancelCommandResponse>, (StatusCode, String)> {
let command = db::get_command_by_id(&state.db, command_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
.ok_or((StatusCode::NOT_FOUND, "Command not found".to_string()))?;
match command.status.as_str() {
"completed" | "failed" | "cancelled" => {
return Err((
StatusCode::BAD_REQUEST,
"Command already finished".to_string(),
));
}
"running" => {
// Update status in DB
db::cancel_command(&state.db, command_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Optionally try to send a cancel signal via WebSocket
let agents = state.agents.read().await;
if agents.is_connected(&command.agent_id) {
let cancel_msg = ServerMessage::Error {
code: "command_cancelled".to_string(),
message: format!("Command {} has been cancelled", command_id),
};
let _ = agents.send_to(&command.agent_id, cancel_msg).await;
}
}
_ => {
// Pending or any other status - just update DB
db::cancel_command(&state.db, command_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
}
}
Ok(Json(CancelCommandResponse {
status: "cancelled".to_string(),
message: "Command cancelled".to_string(),
}))
}
/// Clear history response payload
#[derive(Debug, Serialize)]
pub struct ClearHistoryResponse {
pub deleted: u64,
pub message: String,
}
/// Bulk clear finished commands (completed, failed, cancelled)
/// Requires authentication.
pub async fn clear_command_history(
State(state): State<AppState>,
_user: AuthUser,
) -> Result<Json<ClearHistoryResponse>, (StatusCode, String)> {
let count = db::delete_finished_commands(&state.db)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(ClearHistoryResponse {
deleted: count,
message: format!("Cleared {} commands from history", count),
}))
}

View File

@@ -56,8 +56,9 @@ pub fn routes() -> Router<AppState> {
.route("/metrics/summary", get(metrics::get_summary))
// Commands
.route("/agents/:id/command", post(commands::send_command))
.route("/commands", get(commands::list_commands))
.route("/commands/:id", get(commands::get_command))
.route("/commands", get(commands::list_commands).delete(commands::clear_command_history))
.route("/commands/:id", get(commands::get_command).delete(commands::delete_command))
.route("/commands/:id/cancel", post(commands::cancel_command))
// Legacy Agent (PowerShell for 2008 R2)
.route("/agent/register-legacy", post(agents::register_legacy))
.route("/agent/heartbeat", post(agents::heartbeat))

View File

@@ -161,3 +161,25 @@ pub async fn delete_command(pool: &PgPool, id: Uuid) -> Result<bool, sqlx::Error
.await?;
Ok(result.rows_affected() > 0)
}
/// Cancel a command - set status to 'cancelled' and set completed_at
pub async fn cancel_command(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
sqlx::query(
"UPDATE commands SET status = 'cancelled', completed_at = NOW() WHERE id = $1",
)
.bind(id)
.execute(pool)
.await?;
Ok(())
}
/// Delete all completed, failed, and cancelled commands (bulk clear)
/// Returns the count of deleted rows.
pub async fn delete_finished_commands(pool: &PgPool) -> Result<u64, sqlx::Error> {
let result = sqlx::query(
"DELETE FROM commands WHERE status IN ('completed', 'failed', 'cancelled')",
)
.execute(pool)
.await?;
Ok(result.rows_affected())
}