Added auto deprecation feature

This commit is contained in:
Ultradesu
2025-07-20 17:26:44 +03:00
parent 1eccc0e0f7
commit 9c5518b39e
8 changed files with 937 additions and 9 deletions

View File

@@ -26,6 +26,14 @@
<span class="stat-value" id="totalKeys">0</span>
<span class="stat-label">Total Keys</span>
</div>
<div class="stat-item">
<span class="stat-value" id="activeKeys">0</span>
<span class="stat-label">Active Keys</span>
</div>
<div class="stat-item">
<span class="stat-value deprecated" id="deprecatedKeys">0</span>
<span class="stat-label">Deprecated Keys</span>
</div>
<div class="stat-item">
<span class="stat-value" id="uniqueServers">0</span>
<span class="stat-label">Unique Servers</span>
@@ -34,7 +42,9 @@
<div class="actions-panel">
<button id="addKeyBtn" class="btn btn-primary">Add SSH Key</button>
<button id="scanDnsBtn" class="btn btn-secondary">Scan DNS Resolution</button>
<button id="bulkDeleteBtn" class="btn btn-danger" disabled>Deprecate Selected</button>
<button id="bulkRestoreBtn" class="btn btn-success" disabled style="display: none;">Restore Selected</button>
<button id="bulkPermanentDeleteBtn" class="btn btn-danger" disabled style="display: none;">Delete Selected</button>
<div class="filter-controls">
@@ -129,6 +139,30 @@
</div>
</div>
<!-- DNS Scan Results Modal -->
<div id="dnsScanModal" class="modal">
<div class="modal-content modal-large">
<div class="modal-header">
<h2>DNS Resolution Scan Results</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div id="dnsScanStats" class="scan-stats"></div>
<div id="unresolvedHosts" class="unresolved-hosts">
<div class="section-header">
<h3>Unresolved Hosts</h3>
<button id="selectAllUnresolved" class="btn btn-sm btn-secondary">Select All</button>
</div>
<div id="unresolvedList" class="host-list"></div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" id="closeDnsScan">Close</button>
<button type="button" class="btn btn-danger" id="deprecateUnresolved" disabled>Deprecate Selected</button>
</div>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div id="loadingOverlay" class="loading-overlay">
<div class="loading-spinner"></div>

View File

@@ -39,11 +39,21 @@ class SSHKeyManager {
this.showAddKeyModal();
});
// Scan DNS button
document.getElementById('scanDnsBtn').addEventListener('click', () => {
this.scanDnsResolution();
});
// Bulk delete button
document.getElementById('bulkDeleteBtn').addEventListener('click', () => {
this.deleteSelectedKeys();
});
// Bulk restore button
document.getElementById('bulkRestoreBtn').addEventListener('click', () => {
this.restoreSelectedKeys();
});
// Bulk permanent delete button
document.getElementById('bulkPermanentDeleteBtn').addEventListener('click', () => {
this.permanentlyDeleteSelectedKeys();
@@ -110,6 +120,19 @@ class SSHKeyManager {
this.copyKeyToClipboard();
});
// DNS scan modal
document.getElementById('closeDnsScan').addEventListener('click', () => {
this.hideModal('dnsScanModal');
});
document.getElementById('selectAllUnresolved').addEventListener('click', () => {
this.toggleSelectAllUnresolved();
});
document.getElementById('deprecateUnresolved').addEventListener('click', () => {
this.deprecateSelectedUnresolved();
});
// Close modals when clicking on close button or outside
document.querySelectorAll('.modal .close').forEach(closeBtn => {
closeBtn.addEventListener('click', (e) => {
@@ -219,9 +242,14 @@ class SSHKeyManager {
}
updateStats() {
document.getElementById('totalKeys').textContent = this.keys.length;
const totalKeys = this.keys.length;
const deprecatedKeys = this.keys.filter(key => key.deprecated).length;
const activeKeys = totalKeys - deprecatedKeys;
const uniqueServers = new Set(this.keys.map(key => key.server));
document.getElementById('totalKeys').textContent = totalKeys;
document.getElementById('activeKeys').textContent = activeKeys;
document.getElementById('deprecatedKeys').textContent = deprecatedKeys;
document.getElementById('uniqueServers').textContent = uniqueServers.size;
}
@@ -456,12 +484,15 @@ class SSHKeyManager {
updateBulkDeleteButton() {
const bulkDeleteBtn = document.getElementById('bulkDeleteBtn');
const bulkRestoreBtn = document.getElementById('bulkRestoreBtn');
const bulkPermanentDeleteBtn = document.getElementById('bulkPermanentDeleteBtn');
if (this.selectedKeys.size === 0) {
// No keys selected - hide both buttons
// No keys selected - hide all buttons
bulkDeleteBtn.disabled = true;
bulkDeleteBtn.textContent = 'Deprecate Selected';
bulkRestoreBtn.style.display = 'none';
bulkRestoreBtn.disabled = true;
bulkPermanentDeleteBtn.style.display = 'none';
bulkPermanentDeleteBtn.disabled = true;
return;
@@ -491,6 +522,16 @@ class SSHKeyManager {
bulkDeleteBtn.textContent = 'Deprecate Selected';
}
// Show/hide restore button
if (deprecatedCount > 0) {
bulkRestoreBtn.style.display = 'inline-flex';
bulkRestoreBtn.disabled = false;
bulkRestoreBtn.textContent = `Restore Selected (${deprecatedCount})`;
} else {
bulkRestoreBtn.style.display = 'none';
bulkRestoreBtn.disabled = true;
}
// Show/hide permanent delete button
if (deprecatedCount > 0) {
bulkPermanentDeleteBtn.style.display = 'inline-flex';
@@ -703,6 +744,56 @@ class SSHKeyManager {
}
}
async restoreSelectedKeys() {
if (this.selectedKeys.size === 0) return;
// Filter only deprecated keys
const deprecatedKeys = Array.from(this.selectedKeys).filter(keyId => {
const key = this.findKeyById(keyId);
return key && key.deprecated;
});
if (deprecatedKeys.length === 0) {
this.showToast('No deprecated keys selected', 'warning');
return;
}
if (!confirm(`Are you sure you want to restore ${deprecatedKeys.length} deprecated SSH keys?`)) {
return;
}
// Get unique server names
const serverNames = [...new Set(deprecatedKeys.map(keyId => {
const key = this.findKeyById(keyId);
return key ? key.server : null;
}).filter(Boolean))];
try {
this.showLoading();
const response = await fetch(`/${this.currentFlow}/bulk-restore`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ servers: serverNames })
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || 'Failed to restore keys');
}
const result = await response.json();
this.showToast(result.message, 'success');
await this.loadKeys();
} catch (error) {
this.showToast('Failed to restore selected keys: ' + error.message, 'error');
} finally {
this.hideLoading();
}
}
async permanentlyDeleteSelectedKeys() {
if (this.selectedKeys.size === 0) return;
@@ -805,6 +896,8 @@ class SSHKeyManager {
document.getElementById('keysTableBody').innerHTML = '';
document.getElementById('noKeysMessage').style.display = 'block';
document.getElementById('totalKeys').textContent = '0';
document.getElementById('activeKeys').textContent = '0';
document.getElementById('deprecatedKeys').textContent = '0';
document.getElementById('uniqueServers').textContent = '0';
this.selectedKeys.clear();
this.updateBulkDeleteButton();
@@ -849,6 +942,150 @@ class SSHKeyManager {
}, 300);
}, 4000);
}
// DNS Resolution Scanning
async scanDnsResolution() {
if (!this.currentFlow) {
this.showToast('Please select a flow first', 'warning');
return;
}
try {
this.showLoading();
const response = await fetch(`/${this.currentFlow}/scan-dns`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || 'Failed to scan DNS resolution');
}
const scanResults = await response.json();
this.showDnsScanResults(scanResults);
} catch (error) {
this.showToast('Failed to scan DNS resolution: ' + error.message, 'error');
} finally {
this.hideLoading();
}
}
showDnsScanResults(scanResults) {
const { results, total, unresolved } = scanResults;
// Update stats
const statsDiv = document.getElementById('dnsScanStats');
statsDiv.innerHTML = `
<div class="scan-stat">
<span class="scan-stat-value">${total}</span>
<span class="scan-stat-label">Total Hosts</span>
</div>
<div class="scan-stat">
<span class="scan-stat-value">${total - unresolved}</span>
<span class="scan-stat-label">Resolved</span>
</div>
<div class="scan-stat">
<span class="scan-stat-value unresolved-count">${unresolved}</span>
<span class="scan-stat-label">Unresolved</span>
</div>
`;
// Show unresolved hosts
const unresolvedHosts = results.filter(r => !r.resolved);
const unresolvedList = document.getElementById('unresolvedList');
if (unresolvedHosts.length === 0) {
unresolvedList.innerHTML = '<div class="empty-state">🎉 All hosts resolved successfully!</div>';
document.getElementById('selectAllUnresolved').style.display = 'none';
} else {
document.getElementById('selectAllUnresolved').style.display = 'inline-flex';
unresolvedList.innerHTML = unresolvedHosts.map(host => `
<div class="host-item">
<label>
<input type="checkbox" value="${this.escapeHtml(host.server)}" class="unresolved-checkbox">
<span class="host-name">${this.escapeHtml(host.server)}</span>
</label>
${host.error ? `<span class="host-error">${this.escapeHtml(host.error)}</span>` : ''}
</div>
`).join('');
// Add event listeners to checkboxes
unresolvedList.querySelectorAll('.unresolved-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', () => {
this.updateDeprecateUnresolvedButton();
});
});
}
this.updateDeprecateUnresolvedButton();
this.showModal('dnsScanModal');
}
toggleSelectAllUnresolved() {
const checkboxes = document.querySelectorAll('.unresolved-checkbox');
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
checkboxes.forEach(checkbox => {
checkbox.checked = !allChecked;
});
this.updateDeprecateUnresolvedButton();
}
updateDeprecateUnresolvedButton() {
const selectedCount = document.querySelectorAll('.unresolved-checkbox:checked').length;
const deprecateBtn = document.getElementById('deprecateUnresolved');
if (selectedCount > 0) {
deprecateBtn.disabled = false;
deprecateBtn.textContent = `Deprecate Selected (${selectedCount})`;
} else {
deprecateBtn.disabled = true;
deprecateBtn.textContent = 'Deprecate Selected';
}
}
async deprecateSelectedUnresolved() {
const selectedHosts = Array.from(document.querySelectorAll('.unresolved-checkbox:checked'))
.map(cb => cb.value);
if (selectedHosts.length === 0) {
this.showToast('No hosts selected', 'warning');
return;
}
if (!confirm(`Are you sure you want to deprecate SSH keys for ${selectedHosts.length} unresolved hosts?`)) {
return;
}
try {
this.showLoading();
const response = await fetch(`/${this.currentFlow}/bulk-deprecate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ servers: selectedHosts })
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || 'Failed to deprecate hosts');
}
const result = await response.json();
this.showToast(result.message, 'success');
this.hideModal('dnsScanModal');
await this.loadKeys();
} catch (error) {
this.showToast('Failed to deprecate hosts: ' + error.message, 'error');
} finally {
this.hideLoading();
}
}
}
// Initialize the SSH Key Manager when the page loads

View File

@@ -155,6 +155,10 @@ header h1 {
color: var(--primary-color);
}
.stat-value.deprecated {
color: var(--danger-color);
}
.stat-label {
color: var(--text-secondary);
font-size: 0.875rem;
@@ -663,3 +667,132 @@ input[type="checkbox"]:indeterminate {
.form-group textarea:valid {
border-color: var(--success-color);
}
/* DNS Scan Modal Styles */
.modal-large {
max-width: 800px;
}
.scan-stats {
background: var(--background);
padding: 1rem;
border-radius: var(--border-radius);
margin-bottom: 1.5rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
}
.scan-stat {
text-align: center;
}
.scan-stat-value {
display: block;
font-size: 1.5rem;
font-weight: 600;
color: var(--primary-color);
}
.scan-stat-label {
color: var(--text-secondary);
font-size: 0.875rem;
}
.unresolved-count {
color: var(--danger-color) !important;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.section-header h3 {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
}
.host-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: var(--border-radius);
}
.host-item {
display: flex;
align-items: center;
padding: 0.75rem;
border-bottom: 1px solid var(--border);
transition: background-color 0.2s ease;
}
.host-item:last-child {
border-bottom: none;
}
.host-item:hover {
background-color: var(--background);
}
.host-item label {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
cursor: pointer;
margin: 0;
}
.host-name {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-weight: 500;
color: var(--text-primary);
}
.host-error {
font-size: 0.75rem;
color: var(--danger-color);
margin-left: auto;
max-width: 200px;
word-break: break-word;
}
.empty-state {
text-align: center;
padding: 2rem;
color: var(--text-secondary);
font-style: italic;
}
.scan-progress {
background: var(--background);
padding: 1rem;
border-radius: var(--border-radius);
margin-bottom: 1rem;
text-align: center;
}
.scan-progress-text {
color: var(--text-secondary);
margin-bottom: 0.5rem;
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--primary-color);
transition: width 0.3s ease;
border-radius: 4px;
}