Files
pxe-server/webapp/templates/unattend_editor.html
cproudlock 92c9b0f762 Fix review findings: offline assets, security, audit logging
- Bundle Bootstrap CSS/JS/icons locally for air-gapped operation
- Add path traversal validation on image import source
- Disable Flask debug mode in production
- Fix file handle leaks, remove unused import
- Add python3-pip, python3-venv, p7zip-full to offline packages
- Add pip wheel download/bundling for offline Flask install
- Change UFW default policy from allow to deny
- Fix wrong path displayed in unattend editor template
- Dynamic sidebar image lists from all_image_types
- Add audit logging for all write operations
- Audit log viewer page with activity history

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 16:50:20 -05:00

347 lines
13 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ friendly_name }} - Unattend Editor{% endblock %}
{% block extra_head %}
<style>
.editor-tabs .nav-link {
font-weight: 500;
}
.command-table tbody tr {
transition: background-color 0.15s;
}
.command-table tbody tr.dragging {
opacity: 0.5;
background-color: #e9ecef;
}
.raw-xml-editor {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.85rem;
min-height: 500px;
tab-size: 2;
white-space: pre;
resize: vertical;
}
.section-card {
margin-bottom: 1.5rem;
}
.section-card .card-header {
padding: 0.6rem 1rem;
font-size: 0.95rem;
}
.order-num {
width: 40px;
text-align: center;
font-weight: 600;
color: #6c757d;
}
</style>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-1">{{ friendly_name }}</h2>
<small class="text-muted">
<i class="bi bi-file-earmark-code me-1"></i>
<code>{{ image_type }}/Deploy/FlatUnattendW10.xml</code>
</small>
</div>
<div>
<button type="button" class="btn btn-success" id="saveFormBtn">
<i class="bi bi-floppy me-1"></i> Save
</button>
</div>
</div>
<!-- Tabs -->
<ul class="nav nav-tabs editor-tabs mb-3" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="form-tab" data-bs-toggle="tab"
href="#formView" role="tab">
<i class="bi bi-ui-checks-grid me-1"></i> Form Editor
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="raw-tab" data-bs-toggle="tab"
href="#rawView" role="tab">
<i class="bi bi-code-slash me-1"></i> Raw XML
</a>
</li>
</ul>
<form method="POST" id="unattendForm">
<div class="tab-content">
<!-- ==================== FORM VIEW ==================== -->
<div class="tab-pane fade show active" id="formView" role="tabpanel">
<!-- 1. Driver Paths -->
<div class="card section-card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-motherboard me-1"></i> Driver Paths</span>
<button type="button" class="btn btn-sm btn-outline-primary" id="addDriverPath">
<i class="bi bi-plus-lg"></i> Add
</button>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" id="driverPathsTable">
<thead class="table-light">
<tr>
<th style="width:40px">#</th>
<th>Path</th>
<th style="width:60px"></th>
</tr>
</thead>
<tbody>
{% for dp in data.driver_paths %}
<tr>
<td class="order-num">{{ loop.index }}</td>
<td>
<input type="text" class="form-control form-control-sm"
name="driver_path[]" value="{{ dp }}">
</td>
<td>
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not data.driver_paths %}
<div class="text-center text-muted py-3 empty-message" id="driverPathsEmpty">
No driver paths configured. Click <strong>Add</strong> to add one.
</div>
{% endif %}
</div>
</div>
<!-- 2. Machine Settings -->
<div class="card section-card">
<div class="card-header">
<i class="bi bi-pc-display me-1"></i> Machine Settings
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">Computer Name</label>
<input type="text" class="form-control" name="computer_name"
value="{{ data.computer_name }}" placeholder="* (auto-generate)">
<div class="form-text">Use * to let Windows auto-generate a name.</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Time Zone</label>
<input type="text" class="form-control" name="time_zone"
value="{{ data.time_zone }}" placeholder="Eastern Standard Time">
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Registered Organization</label>
<input type="text" class="form-control" name="registered_organization"
value="{{ data.registered_organization }}" placeholder="GE Aerospace">
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Registered Owner</label>
<input type="text" class="form-control" name="registered_owner"
value="{{ data.registered_owner }}" placeholder="GE Aerospace">
</div>
</div>
</div>
</div>
<!-- 3. Specialize Commands (RunSynchronous) -->
<div class="card section-card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-terminal me-1"></i> Specialize Commands (RunSynchronous)</span>
<button type="button" class="btn btn-sm btn-outline-primary" id="addSpecCmd">
<i class="bi bi-plus-lg"></i> Add
</button>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0 command-table" id="specCmdTable">
<thead class="table-light">
<tr>
<th style="width:30px"></th>
<th style="width:50px">Order</th>
<th>Path / Command</th>
<th>Description</th>
<th style="width:90px"></th>
</tr>
</thead>
<tbody>
{% for cmd in data.specialize_commands %}
<tr draggable="true">
<td class="drag-handle"><i class="bi bi-grip-vertical"></i></td>
<td class="order-num">{{ loop.index }}</td>
<td>
<input type="text" class="form-control form-control-sm"
name="spec_cmd_path[]" value="{{ cmd.path }}">
</td>
<td>
<input type="text" class="form-control form-control-sm"
name="spec_cmd_desc[]" value="{{ cmd.description }}">
</td>
<td class="text-nowrap">
<button type="button" class="btn btn-outline-secondary btn-row-action move-up" title="Move up">
<i class="bi bi-arrow-up"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down">
<i class="bi bi-arrow-down"></i>
</button>
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not data.specialize_commands %}
<div class="text-center text-muted py-3 empty-message" id="specCmdEmpty">
No specialize commands configured. Click <strong>Add</strong> to add one.
</div>
{% endif %}
</div>
</div>
<!-- 4. OOBE Settings -->
<div class="card section-card">
<div class="card-header">
<i class="bi bi-shield-check me-1"></i> OOBE Settings
</div>
<div class="card-body">
<div class="row g-3">
{% set bool_oobe_fields = [
("HideEULAPage", "Hide EULA Page"),
("HideOEMRegistrationScreen", "Hide OEM Registration Screen"),
("HideOnlineAccountScreens", "Hide Online Account Screens"),
("HideWirelessSetupInOOBE", "Hide Wireless Setup in OOBE"),
("HideLocalAccountScreen", "Hide Local Account Screen"),
("SkipUserOOBE", "Skip User OOBE"),
("SkipMachineOOBE", "Skip Machine OOBE"),
] %}
{% for key, label in bool_oobe_fields %}
<div class="col-md-4">
<div class="form-check form-switch">
<input class="form-check-input oobe-toggle" type="checkbox"
id="oobe_{{ key }}"
data-field="oobe_{{ key }}"
{% if data.oobe[key]|lower == 'true' %}checked{% endif %}>
<label class="form-check-label" for="oobe_{{ key }}">{{ label }}</label>
<input type="hidden" name="oobe_{{ key }}" id="oobe_{{ key }}_val"
value="{{ data.oobe[key] }}">
</div>
</div>
{% endfor %}
<div class="col-md-4">
<label class="form-label fw-semibold">Network Location</label>
<select class="form-select form-select-sm" name="oobe_NetworkLocation">
{% for opt in ["Home", "Work", "Other"] %}
<option value="{{ opt }}" {% if data.oobe.NetworkLocation == opt %}selected{% endif %}>
{{ opt }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">ProtectYourPC</label>
<select class="form-select form-select-sm" name="oobe_ProtectYourPC">
{% for opt in ["1", "2", "3"] %}
<option value="{{ opt }}" {% if data.oobe.ProtectYourPC == opt %}selected{% endif %}>
{{ opt }}{% if opt == "1" %} (Recommended){% elif opt == "3" %} (Skip){% endif %}
</option>
{% endfor %}
</select>
<div class="form-text">1 = Recommended, 2 = Install only updates, 3 = Skip</div>
</div>
</div>
</div>
</div>
<!-- 5. First Logon Commands -->
<div class="card section-card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-play-circle me-1"></i> First Logon Commands</span>
<button type="button" class="btn btn-sm btn-outline-primary" id="addFlCmd">
<i class="bi bi-plus-lg"></i> Add
</button>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0 command-table" id="flCmdTable">
<thead class="table-light">
<tr>
<th style="width:30px"></th>
<th style="width:50px">Order</th>
<th>Command Line</th>
<th>Description</th>
<th style="width:90px"></th>
</tr>
</thead>
<tbody>
{% for cmd in data.firstlogon_commands %}
<tr draggable="true">
<td class="drag-handle"><i class="bi bi-grip-vertical"></i></td>
<td class="order-num">{{ loop.index }}</td>
<td>
<input type="text" class="form-control form-control-sm"
name="fl_cmd_commandline[]" value="{{ cmd.commandline }}">
</td>
<td>
<input type="text" class="form-control form-control-sm"
name="fl_cmd_desc[]" value="{{ cmd.description }}">
</td>
<td class="text-nowrap">
<button type="button" class="btn btn-outline-secondary btn-row-action move-up" title="Move up">
<i class="bi bi-arrow-up"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down">
<i class="bi bi-arrow-down"></i>
</button>
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not data.firstlogon_commands %}
<div class="text-center text-muted py-3 empty-message" id="flCmdEmpty">
No first logon commands configured. Click <strong>Add</strong> to add one.
</div>
{% endif %}
</div>
</div>
</div><!-- end formView -->
<!-- ==================== RAW XML VIEW ==================== -->
<div class="tab-pane fade" id="rawView" role="tabpanel">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-code-slash me-1"></i> Raw XML</span>
<button type="button" class="btn btn-sm btn-success" id="saveRawBtn">
<i class="bi bi-floppy me-1"></i> Save Raw XML
</button>
</div>
<div class="card-body">
<textarea class="form-control raw-xml-editor" name="raw_xml"
id="rawXmlEditor">{{ data.raw_xml }}</textarea>
</div>
</div>
</div>
</div><!-- end tab-content -->
<input type="hidden" name="save_mode" id="saveMode" value="form">
</form>
{% endblock %}
{% block extra_scripts %}
<script>
// Pass the image_type and API URL to JavaScript
window.PXE_IMAGE_TYPE = "{{ image_type }}";
window.PXE_API_URL = "{{ url_for('api_save_unattend', image_type=image_type) }}";
</script>
{% endblock %}