Files
P-Aimon-Pen-ROFMAD/templates/action_builder.html
T
2026-06-18 15:03:00 +02:00

1034 lines
36 KiB
HTML
Executable File

{% extends "base.html" %}
{% block extra_head %}
<style>
/* Action Builder Page */
.action-builder { max-width: 1200px; margin: 0 auto; }
.builder-subtitle { margin-top: 0.5rem; }
.builder-subtitle p { color: var(--text-muted); font-size: 0.82rem; margin: 0; }
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1.5rem;
}
.conditions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
}
.condition-group {
background-color: var(--gb-bg);
padding: 1.25rem;
border: 1px solid var(--gb-bg1);
}
.condition-group h4 {
color: var(--gb-blue-b);
margin-bottom: 0.75rem;
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
border-bottom: 1px solid var(--gb-bg1);
padding-bottom: 0.35rem;
}
.tag-input-group { display: flex; gap: 0.5rem; }
.tag-input-group input { flex: 1; }
.checkbox-group { display: flex; flex-direction: column; gap: 0.6rem; }
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
color: var(--text-secondary);
font-size: 0.82rem;
}
.checkbox-label input[type="checkbox"] {
width: auto;
margin-right: 0.6rem;
cursor: pointer;
accent-color: var(--gb-blue);
}
.tag-patterns-container { min-height: 80px; margin-bottom: 0.75rem; }
.tag-pattern-item {
display: flex;
gap: 0.75rem;
align-items: center;
background-color: var(--gb-bg-h);
border: 1px solid var(--gb-bg2);
padding: 0.6rem 0.875rem;
margin-bottom: 1px;
}
.tag-pattern-item input { flex: 1; margin-bottom: 0; }
.tag-pattern-remove {
background-color: var(--gb-red);
color: var(--gb-fg);
border: 1px solid var(--gb-red-b);
border-radius: 0;
padding: 0.3rem 0.6rem;
cursor: pointer;
font-size: 0.72rem;
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
text-transform: uppercase;
}
.tag-pattern-remove:hover { background-color: var(--gb-red-b); }
.preview-container { margin-bottom: 1.5rem; }
.preview-stats { text-align: center; margin-bottom: 1.5rem; }
.stat-item {
display: inline-flex;
flex-direction: column;
align-items: center;
background-color: var(--gb-bg);
border: 1px solid var(--gb-bg1);
border-left: 2px solid var(--gb-blue);
padding: 1rem 1.5rem;
min-width: 180px;
}
.stat-number {
font-size: 1.8rem;
font-weight: 700;
color: var(--gb-blue-b);
margin-bottom: 0.2rem;
font-family: 'JetBrains Mono', monospace;
}
.stat-label { color: var(--text-muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; }
.preview-actions { display: flex; gap: 0.75rem; justify-content: center; margin-bottom: 1.5rem; }
.target-hosts-list {
max-height: 280px;
overflow-y: auto;
background-color: var(--gb-bg-h);
border: 1px solid var(--gb-bg1);
padding: 0;
margin-top: 0.75rem;
}
.target-host-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.45rem 0.875rem;
border-bottom: 1px solid var(--gb-bg1);
font-size: 0.82rem;
}
.target-host-item:last-child { border-bottom: none; }
.target-host-item:hover { background-color: var(--gb-bg1); }
.host-ip { color: var(--gb-blue-b); font-weight: 700; font-family: 'JetBrains Mono', monospace; }
.host-tags { display: flex; gap: 0.2rem; flex-wrap: wrap; }
.host-tag {
background-color: var(--gb-blue);
color: var(--gb-fg);
padding: 0.1rem 0.4rem;
font-size: 0.65rem;
font-weight: 600;
border-radius: 0;
}
.selected-tags-display {
margin-top: 0.75rem;
padding: 0.75rem;
background-color: var(--gb-bg-h);
min-height: 44px;
border: 1px solid var(--gb-bg2);
}
.selected-tag-item {
display: inline-flex;
align-items: center;
background-color: var(--gb-yellow);
color: var(--gb-bg-h);
padding: 0.15rem 0.5rem;
margin: 0.2rem;
font-size: 0.75rem;
font-weight: 700;
border-radius: 0;
font-family: 'JetBrains Mono', monospace;
}
.selected-tag-remove {
background: none;
border: none;
color: inherit;
margin-left: 0.4rem;
cursor: pointer;
font-weight: 700;
}
.tag-browser-modal { max-width: 800px; max-height: 80vh; overflow: hidden; }
.modal-body { max-height: 60vh; overflow-y: auto; }
.tag-browser-search {
margin-bottom: 0.75rem;
position: sticky;
top: 0;
background: var(--gb-bg);
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--gb-bg1);
}
.tag-browser-content { padding: 0.75rem 0; }
.browsable-tag {
display: inline-block;
background-color: var(--gb-bg1);
color: var(--text-secondary);
padding: 0.25rem 0.6rem;
margin: 0.2rem;
cursor: pointer;
transition: background-color 0.1s, color 0.1s;
border: 1px solid var(--gb-bg2);
font-size: 0.78rem;
font-family: 'JetBrains Mono', monospace;
border-radius: 0;
}
.browsable-tag:hover {
background-color: var(--gb-blue);
color: var(--gb-fg);
border-color: var(--gb-blue-b);
}
.browsable-tag.selected {
background-color: var(--gb-yellow);
color: var(--gb-bg-h);
border-color: var(--gb-yellow-b);
font-weight: 700;
}
.execution-status {
background-color: var(--gb-bg);
border: 1px solid var(--gb-bg1);
border-left: 3px solid var(--gb-blue);
padding: 1.5rem;
text-align: center;
}
.execution-progress { margin-bottom: 0.75rem; }
.progress-text { color: var(--text-secondary); margin-bottom: 0.4rem; font-size: 0.82rem; }
.progress-bar { background: var(--gb-bg1); height: 8px; overflow: hidden; border: 1px solid var(--gb-bg2); }
.progress-fill { background-color: var(--gb-blue); height: 100%; transition: width 0.3s ease; }
/* Builder terminal — professional, no glow, no green */
.builder-terminal-container {
background-color: var(--gb-bg-h);
border: 1px solid var(--gb-blue);
border-top: 2px solid var(--gb-blue-b);
font-family: 'JetBrains Mono', 'Consolas', monospace;
margin-top: 1.25rem;
overflow: hidden;
}
.builder-terminal-header {
background-color: var(--gb-bg1);
padding: 0.6rem 1rem;
border-bottom: 1px solid var(--gb-bg2);
display: flex;
align-items: center;
justify-content: space-between;
}
.builder-terminal-title {
color: var(--gb-blue-b);
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.builder-terminal-body { padding: 1rem; background-color: var(--gb-bg-h); }
.builder-terminal-form { margin-bottom: 0; }
.builder-terminal-input-line {
display: flex;
align-items: center;
gap: 0.75rem;
background-color: var(--gb-bg);
padding: 0.75rem;
border: 1px solid var(--gb-bg2);
}
.builder-terminal-prompt {
color: var(--gb-blue-b);
font-weight: 700;
font-size: 1rem;
user-select: none;
}
.builder-terminal-input {
flex: 1;
background-color: var(--gb-bg-h);
border: 1px solid var(--gb-bg2);
color: var(--gb-fg);
font-family: inherit;
font-size: 0.88rem;
padding: 0.5rem 0.75rem;
border-radius: 0;
outline: none;
transition: border-color 0.1s;
width: auto;
}
.builder-terminal-input:focus {
border-color: var(--gb-blue);
box-shadow: 0 0 0 1px var(--gb-blue);
}
.builder-terminal-input::placeholder { color: var(--text-dim); font-style: italic; }
.builder-terminal-timeout {
background-color: var(--gb-bg-h);
border: 1px solid var(--gb-bg3);
color: var(--text-secondary);
padding: 0.5rem 0.6rem;
border-radius: 0;
width: 72px;
font-size: 0.85rem;
text-align: center;
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
width: auto;
}
.builder-terminal-timeout:focus { border-color: var(--gb-blue); outline: none; }
.builder-terminal-run-btn {
background-color: var(--gb-blue);
color: var(--gb-fg);
border: 1px solid var(--gb-blue-b);
padding: 0.5rem 1.25rem;
border-radius: 0;
cursor: pointer;
font-weight: 700;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
font-family: 'JetBrains Mono', monospace;
transition: background-color 0.1s, color 0.1s;
}
.builder-terminal-run-btn:hover { background-color: var(--gb-blue-b); color: var(--gb-bg-h); }
.builder-terminal-run-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.builder-terminal-output {
background-color: var(--gb-bg-h);
color: var(--gb-fg3);
padding: 0.875rem;
font-family: inherit;
white-space: pre-wrap;
max-height: 420px;
overflow-y: auto;
margin-bottom: 0.875rem;
border: 1px solid var(--gb-bg2);
border-top: none;
line-height: 1.5;
font-size: 0.82rem;
}
.output-actions { display: flex; gap: 0.75rem; justify-content: flex-end; }
.output-actions .btn {
padding: 0.45rem 1rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 0.78rem;
border-radius: 0;
border: 1px solid transparent;
}
.output-actions .btn-success {
background-color: var(--gb-blue);
color: var(--gb-fg);
border-color: var(--gb-blue-b);
}
.output-actions .btn-success:hover { background-color: var(--gb-blue-b); color: var(--gb-bg-h); }
.output-actions .btn-secondary {
background-color: var(--gb-bg1);
color: var(--text-secondary);
border-color: var(--gb-bg2);
}
.output-actions .btn-secondary:hover { background-color: var(--gb-bg2); color: var(--text-primary); }
.builder-terminal-info {
background-color: var(--gb-bg);
border: 1px solid var(--gb-bg1);
border-left: 3px solid var(--gb-blue);
padding: 0.875rem 1rem;
margin-bottom: 1.25rem;
}
.builder-terminal-info p { margin: 0.4rem 0; font-size: 0.82rem; color: var(--text-primary); }
.builder-terminal-info code {
background-color: var(--gb-bg1);
color: var(--gb-blue-b);
padding: 0.1rem 0.35rem;
border: 1px solid var(--gb-bg2);
border-radius: 0;
}
@media (max-width: 768px) {
.form-grid { grid-template-columns: 1fr; gap: 1rem; }
.conditions-grid { grid-template-columns: 1fr; }
.tag-input-group { flex-direction: column; }
.preview-actions { flex-direction: column; align-items: stretch; }
.tag-pattern-item { flex-direction: column; align-items: stretch; }
.builder-terminal-input-line { flex-direction: column; align-items: stretch; }
}
</style>
{% endblock %}
{% block title %}Action Builder{% endblock %}
{% block content %}
<div class="action-builder">
<div class="section-header">
<h1>Live Action Builder</h1>
<div class="builder-subtitle">
<p>Create and execute custom actions dynamically. Build actions that mirror your workflow.yaml structure.</p>
</div>
</div>
<!-- Action Definition Card -->
<div class="section-card">
<div class="section-header">
<h2>Action Definition</h2>
<p class="section-subtitle">Define the basic action properties and command</p>
</div>
<div class="section-content">
<form id="actionBuilderForm">
<div class="form-group">
<label for="actionName">Action Name <span class="required">*</span></label>
<input type="text" id="actionName" name="name" placeholder="e.g., smb_enumeration" required>
<small class="form-help">Unique identifier for this action</small>
</div>
<div class="form-group">
<label for="actionCommand">Command <span class="required">*</span></label>
<textarea id="actionCommand" name="command" rows="2" placeholder="nxc smb &#123;&#123;IP&#125;&#125; -u '' -p '' --shares" required></textarea>
<small class="form-help">Full command — Placeholders: &#123;&#123;IP&#125;&#125; &#123;&#123;PORT&#125;&#125; &#123;&#123;URL&#125;&#125; &#123;&#123;PROTOCOL&#125;&#125; — per-instance: &#123;&#123;SERVICE_PORT&#125;&#125; &#123;&#123;HTTP_URL&#125;&#125; &#123;&#123;HTTP_PORT&#125;&#125; &#123;&#123;HTTP_PROTOCOL&#125;&#125;</small>
</div>
<div class="form-group">
<label for="actionTimeout">Timeout (seconds)</label>
<input type="number" id="actionTimeout" name="timeout_s" min="30" max="3600" value="300">
<small class="form-help">Maximum execution time per host</small>
</div>
</form>
</div>
</div>
<!-- Conditions Card -->
<div class="section-card">
<div class="section-header">
<h2>Execution Conditions</h2>
<p class="section-subtitle">Define when this action should execute</p>
</div>
<div class="section-content">
<div class="conditions-grid">
<!-- Port Conditions -->
<div class="condition-group">
<h4>Port Conditions</h4>
<div class="form-group">
<label for="portContains">Contains Ports</label>
<input type="text" id="portContains" name="contains" placeholder="e.g., 445 AND 139 or 80 OR 443">
<small class="form-help">Use AND/OR logic for multiple ports</small>
</div>
<div class="form-group">
<label for="portEquals">Equals Port </label>
<input type="text" id="portEquals" name="equals" placeholder="e.g., 22">
<small class="form-help">Exact and strict port match, ex : 22 -> host with only port 22 opened will be targeted !</small>
</div>
</div>
<!-- Tag Conditions -->
<div class="condition-group">
<h4>Tag Conditions</h4>
<div class="form-group">
<label for="tagCondition">Has Tags</label>
<div class="tag-input-group">
<input type="text" id="tagCondition" name="has_tag" placeholder="e.g., windows AND smb or webserver">
<button type="button" class="btn btn-secondary" id="browseTagsBtn">Browse Tags</button>
</div>
<small class="form-help">Use AND/OR logic for multiple tags</small>
</div>
<div class="selected-tags-display" id="selectedTagsDisplay">
<!-- Selected tags will appear here -->
</div>
</div>
<!-- Service Conditions -->
<div class="condition-group">
<h4>Service Conditions</h4>
<div class="form-group">
<label for="serviceCondition">Has Service</label>
<input type="text" id="serviceCondition" name="has_service" placeholder="e.g., mssql or rdp OR winrm">
<small class="form-help">Single name → per-port fan-out. Use exact name: mssql, rdp, smb, ftp… AND/OR = boolean gate only.</small>
</div>
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" id="hasHttpService" name="has_http_service">
<span class="checkmark"></span>
Requires HTTP Services (per-service fan-out)
</label>
<label class="checkbox-label">
<input type="checkbox" id="reconComplete" name="reconnaissance_complete" checked>
<span class="checkmark"></span>
Requires Reconnaissance Complete
</label>
<label class="checkbox-label">
<input type="checkbox" id="saveToWorkflow" name="save_to_workflow">
<span class="checkmark"></span>
Save to workflow file after execution
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Tag Patterns Card -->
<div class="section-card">
<div class="section-header">
<h2>Output Tag Patterns</h2>
<p class="section-subtitle">Define what tags to apply based on text found in command output</p>
</div>
<div class="section-content">
<div class="tag-patterns-container">
<div class="tag-pattern-list" id="tagPatternList">
<!-- Tag patterns will be added here -->
</div>
<button type="button" class="btn btn-secondary" id="addTagPatternBtn">+ Add Tag Pattern</button>
</div>
</div>
</div>
<!-- Preview Card -->
<div class="section-card">
<div class="section-header">
<h2>Target Preview</h2>
<p class="section-subtitle">Hosts that will be targeted by this action</p>
</div>
<div class="section-content">
<div class="preview-container">
<div class="preview-stats">
<div class="stat-item">
<span class="stat-number" id="targetCount">0</span>
<span class="stat-label">Hosts will be targeted</span>
</div>
</div>
<div class="preview-actions">
<button type="button" class="btn btn-secondary" id="refreshPreviewBtn">Refresh Preview</button>
<button type="button" class="btn btn-primary" id="executeActionBtn" disabled>Execute Action</button>
</div>
</div>
<div class="target-hosts-list" id="targetHostsList">
<!-- Target hosts will appear here -->
</div>
</div>
</div>
<!-- Results Card -->
<div class="section-card" id="resultsCard" style="display: none;">
<div class="section-header">
<h2>Execution Results</h2>
<p class="section-subtitle">Real-time action execution status</p>
</div>
<div class="section-content">
<div class="execution-status" id="executionStatus">
<!-- Execution status will appear here -->
</div>
</div>
</div>
</div>
<!-- Tag Browser Modal -->
<div id="tagBrowserModal" class="modal" style="display: none;">
<div class="modal-content tag-browser-modal">
<div class="modal-header">
<h3>Browse Available Tags</h3>
<span class="modal-close" onclick="closeTagBrowser()">&times;</span>
</div>
<div class="modal-body">
<div class="tag-browser-search">
<input type="text" id="tagSearchInput" placeholder="Filter tags..." class="search-input">
</div>
<div class="tag-browser-content" id="tagBrowserContent">
<!-- Tags will be loaded here -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeTagBrowser()">Cancel</button>
<button type="button" class="btn btn-primary" onclick="applySelectedTags()">Apply Selected Tags</button>
</div>
</div>
</div>
<script>
// Action Builder JavaScript
class ActionBuilder {
constructor() {
this.selectedTags = new Set();
this.tagPatterns = [];
this.availableTags = [];
this.init();
}
init() {
this.bindEvents();
this.loadAvailableTags();
this.addInitialTagPattern();
}
bindEvents() {
// Check if elements exist before binding events
const actionForm = document.getElementById('actionBuilderForm');
if (actionForm) {
actionForm.addEventListener('input', () => {
this.updatePreview();
});
}
// Condition changes - check each element exists
['portContains', 'portEquals', 'tagCondition', 'serviceCondition', 'hasHttpService', 'reconComplete'].forEach(id => {
const element = document.getElementById(id);
if (element) {
element.addEventListener('input', () => this.updatePreview());
element.addEventListener('change', () => this.updatePreview());
}
});
// Tag browser
const browseBtn = document.getElementById('browseTagsBtn');
if (browseBtn) {
browseBtn.addEventListener('click', () => {
this.openTagBrowser();
});
}
// Tag patterns
const addPatternBtn = document.getElementById('addTagPatternBtn');
if (addPatternBtn) {
addPatternBtn.addEventListener('click', () => {
this.addTagPattern();
});
}
// Action buttons
const refreshBtn = document.getElementById('refreshPreviewBtn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
this.updatePreview();
});
}
const executeBtn = document.getElementById('executeActionBtn');
if (executeBtn) {
executeBtn.addEventListener('click', () => {
this.executeAction();
});
}
// Tag search
const tagSearchInput = document.getElementById('tagSearchInput');
if (tagSearchInput) {
tagSearchInput.addEventListener('input', (e) => {
this.filterTags(e.target.value);
});
}
}
async loadAvailableTags() {
try {
const response = await fetch('/api/all-tags');
if (response.ok) {
const data = await response.json();
this.availableTags = [
...data.manual_tags,
...data.refined_tags,
...data.automated_tags
];
}
} catch (error) {
console.error('Failed to load tags:', error);
}
}
openTagBrowser() {
// Populate tag browser
const content = document.getElementById('tagBrowserContent');
if (content) {
content.innerHTML = this.availableTags.map(tag => `
<span class="browsable-tag" data-tag="${tag.name}" onclick="actionBuilder.toggleTag('${tag.name}')">
${tag.name} (${tag.count})
</span>
`).join('');
}
// Show modal with proper display
const modal = document.getElementById('tagBrowserModal');
if (modal) {
modal.style.display = 'flex';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
// Add backdrop click to close
modal.addEventListener('click', (e) => {
if (e.target === modal) {
this.closeTagBrowser();
}
});
}
}
toggleTag(tagName) {
if (this.selectedTags.has(tagName)) {
this.selectedTags.delete(tagName);
// Remove selected styling
const tagElement = document.querySelector(`[data-tag="${tagName}"]`);
if (tagElement) {
tagElement.classList.remove('selected');
}
} else {
this.selectedTags.add(tagName);
// Add selected styling
const tagElement = document.querySelector(`[data-tag="${tagName}"]`);
if (tagElement) {
tagElement.classList.add('selected');
}
}
}
closeTagBrowser() {
const modal = document.getElementById('tagBrowserModal');
if (modal) {
modal.style.display = 'none';
}
}
applySelectedTags() {
// Build tag condition string
const tagArray = Array.from(this.selectedTags);
const tagCondition = tagArray.join(' OR ');
const tagConditionInput = document.getElementById('tagCondition');
if (tagConditionInput) {
tagConditionInput.value = tagCondition;
}
// Update display
this.updateSelectedTagsDisplay();
this.closeTagBrowser();
this.updatePreview();
}
updateSelectedTagsDisplay() {
const display = document.getElementById('selectedTagsDisplay');
if (!display) return;
if (this.selectedTags.size === 0) {
display.innerHTML = '<p style="color: var(--text-muted); text-align: center; margin: 1rem 0;">No tags selected. Click "Browse Tags" to select tags.</p>';
} else {
display.innerHTML = Array.from(this.selectedTags).map(tag => `
<span class="selected-tag-item">
${tag}
<button class="selected-tag-remove" onclick="actionBuilder.removeSelectedTag('${tag}')">&times;</button>
</span>
`).join('');
}
}
removeSelectedTag(tagName) {
this.selectedTags.delete(tagName);
this.updateSelectedTagsDisplay();
// Update the input field
const remaining = Array.from(this.selectedTags);
const tagConditionInput = document.getElementById('tagCondition');
if (tagConditionInput) {
tagConditionInput.value = remaining.join(' OR ');
}
this.updatePreview();
}
filterTags(query) {
const tags = document.querySelectorAll('.browsable-tag');
tags.forEach(tag => {
const tagName = tag.getAttribute('data-tag').toLowerCase();
if (tagName.includes(query.toLowerCase())) {
tag.style.display = 'inline-block';
} else {
tag.style.display = 'none';
}
});
}
addInitialTagPattern() {
this.addTagPattern();
}
addTagPattern() {
const container = document.getElementById('tagPatternList');
if (!container) return;
const patternId = `pattern_${Date.now()}`;
const patternHtml = `
<div class="tag-pattern-item" id="${patternId}">
<div class="form-group" style="margin-bottom: 0; flex: 1;">
<input type="text" placeholder="Text to find (e.g., '[+]' or 'STATUS_VULN')" data-type="pattern">
</div>
<div class="form-group" style="margin-bottom: 0; flex: 1;">
<input type="text" placeholder="Tag to apply (e.g., 'vulnerable')" data-type="tag">
</div>
<button type="button" class="tag-pattern-remove" onclick="actionBuilder.removeTagPattern('${patternId}')">
X
</button>
</div>
`;
container.insertAdjacentHTML('beforeend', patternHtml);
}
removeTagPattern(patternId) {
const element = document.getElementById(patternId);
if (element) {
element.remove();
}
}
getTagPatterns() {
const patterns = [];
document.querySelectorAll('.tag-pattern-item').forEach(item => {
const pattern = item.querySelector('[data-type="pattern"]').value.trim();
const tag = item.querySelector('[data-type="tag"]').value.trim();
if (pattern && tag) {
patterns.push({ pattern, tag });
}
});
return patterns;
}
async updatePreview() {
// Validate form first
const isValid = this.validateForm();
const executeBtn = document.getElementById('executeActionBtn');
if (executeBtn) {
executeBtn.disabled = !isValid;
}
const targetCountEl = document.getElementById('targetCount');
const targetHostsList = document.getElementById('targetHostsList');
if (!isValid) {
if (targetCountEl) targetCountEl.textContent = '0';
if (targetHostsList) {
targetHostsList.innerHTML = '<p style="color: var(--text-muted); text-align: center;">Complete the form to see target preview</p>';
}
return;
}
// Build condition object
const condition = this.buildCondition();
try {
const response = await fetch('/actions/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ condition })
});
if (response.ok) {
const data = await response.json();
this.updatePreviewDisplay(data.hosts);
}
} catch (error) {
console.error('Preview failed:', error);
}
}
updatePreviewDisplay(hosts) {
const targetCountEl = document.getElementById('targetCount');
const hostsList = document.getElementById('targetHostsList');
if (targetCountEl) {
targetCountEl.textContent = hosts.length;
}
if (hostsList) {
if (hosts.length === 0) {
hostsList.innerHTML = '<p style="color: var(--text-muted); text-align: center;">No hosts match the current conditions</p>';
} else {
hostsList.innerHTML = hosts.map(host => `
<div class="target-host-item">
<div>
<span class="host-ip">${host.ip}</span>
${host.hostname ? `<span style="color: var(--text-muted); margin-left: 0.5rem;">(${host.hostname})</span>` : ''}
</div>
<div class="host-tags">
${host.tags.slice(0, 3).map(tag => `<span class="host-tag">${tag}</span>`).join('')}
${host.tags.length > 3 ? `<span class="host-tag">+${host.tags.length - 3} more</span>` : ''}
</div>
</div>
`).join('');
}
}
}
validateForm() {
const nameEl = document.getElementById('actionName');
const commandEl = document.getElementById('actionCommand');
const name = nameEl ? nameEl.value.trim() : '';
const command = commandEl ? commandEl.value.trim() : '';
return name && command;
}
buildCondition() {
const condition = {};
const containsEl = document.getElementById('portContains');
const contains = containsEl ? containsEl.value.trim() : '';
if (contains) condition.contains = contains;
const equalsEl = document.getElementById('portEquals');
const equals = equalsEl ? equalsEl.value.trim() : '';
if (equals) condition.equals = equals;
const hasTagEl = document.getElementById('tagCondition');
const hasTag = hasTagEl ? hasTagEl.value.trim() : '';
if (hasTag) condition.has_tag = hasTag;
const hasServiceEl = document.getElementById('serviceCondition');
const hasService = hasServiceEl ? hasServiceEl.value.trim() : '';
if (hasService) condition.has_service = hasService;
const hasHttpServiceEl = document.getElementById('hasHttpService');
const hasHttpService = hasHttpServiceEl ? hasHttpServiceEl.checked : false;
if (hasHttpService) condition.has_http_service = true;
const reconCompleteEl = document.getElementById('reconComplete');
const reconComplete = reconCompleteEl ? reconCompleteEl.checked : true;
if (reconComplete) condition.reconnaissance_complete = true;
return condition;
}
async executeAction() {
const saveToWorkflowEl = document.getElementById('saveToWorkflow');
const shouldSave = saveToWorkflowEl ? saveToWorkflowEl.checked : false;
const actionNameEl = document.getElementById('actionName');
const actionCommandEl = document.getElementById('actionCommand');
const actionTimeoutEl = document.getElementById('actionTimeout');
const actionData = {
name: actionNameEl ? actionNameEl.value.trim() : '',
command: actionCommandEl ? actionCommandEl.value.trim() : '',
timeout_s: actionTimeoutEl ? parseInt(actionTimeoutEl.value) || 300 : 300,
condition: this.buildCondition(),
tags: this.getTagPatterns(),
save_to_workflow: shouldSave
};
// Show confirmation
const targetCountEl = document.getElementById('targetCount');
const targetCount = targetCountEl ? targetCountEl.textContent : '0';
if (!confirm(`Execute "${actionData.name}" on ${targetCount} hosts?`)) {
return;
}
// Show results card
const resultsCard = document.getElementById('resultsCard');
const executionStatus = document.getElementById('executionStatus');
if (resultsCard) {
resultsCard.style.display = 'block';
}
if (executionStatus) {
executionStatus.innerHTML = `
<div class="execution-progress">
<div class="progress-text">Executing action on ${targetCount} hosts...</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 0%"></div>
</div>
</div>
`;
}
// Disable execute button
const executeBtn = document.getElementById('executeActionBtn');
if (executeBtn) {
executeBtn.disabled = true;
executeBtn.textContent = 'Executing...';
}
try {
const response = await fetch('/actions/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(actionData)
});
const result = await response.json();
if (result.success) {
if (executionStatus) {
executionStatus.innerHTML = `
<div style="color: var(--success-green);">
<h3>Action Executed Successfully</h3>
<p>Action "${actionData.name}" has been queued for execution on ${targetCount} hosts.</p>
<p>Results will appear in the host detail pages as they complete.</p>
<a href="/hosts" class="btn btn-primary">View Hosts</a>
</div>
`;
}
} else {
throw new Error(result.message || 'Execution failed');
}
} catch (error) {
if (executionStatus) {
executionStatus.innerHTML = `
<div style="color: var(--danger-red);">
<h3>Execution Failed</h3>
<p>Error: ${error.message}</p>
<button class="btn btn-secondary" onclick="location.reload()">Try Again</button>
</div> name: actionNameEl ? actionNameEl.value.trim() : '',
`;
}
} finally {
// Re-enable execute button
if (executeBtn) {
executeBtn.disabled = false;
executeBtn.textContent = 'Execute Action';
}
}
}
}
// Global functions for template
function closeTagBrowser() {
if (window.actionBuilder) {
window.actionBuilder.closeTagBrowser();
}
}
function applySelectedTags() {
if (window.actionBuilder) {
window.actionBuilder.applySelectedTags();
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
window.actionBuilder = new ActionBuilder();
});
</script>
{% endblock %}