mirror of
https://github.com/P-Aimon-Pen/ROFMAD
synced 2026-06-21 13:45:02 +00:00
1034 lines
36 KiB
HTML
Executable File
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 {{IP}} -u '' -p '' --shares" required></textarea>
|
|
<small class="form-help">Full command — Placeholders: {{IP}} {{PORT}} {{URL}} {{PROTOCOL}} — per-instance: {{SERVICE_PORT}} {{HTTP_URL}} {{HTTP_PORT}} {{HTTP_PROTOCOL}}</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()">×</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}')">×</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 %} |