SGO/templates/import.html
Eduardo Figueroa a6b2cea31f
Some checks failed
CI / syntax-check (push) Has been cancelled
CI / security-scan (push) Has been cancelled
CI / container-lint (push) Has been cancelled
CI / container-build (push) Has been cancelled
Publish / publish (push) Has been cancelled
Migrate to Podman, Forgejo Actions; clean up cruft
Container:
- Dockerfile → Containerfile; drop gosu, entrypoint, PUID/PGID user-switching
- HOME=/config so Path.home()/.aws resolves to runtime-mounted credentials
- docker-compose.yml → compose.yml with userns_mode: keep-id for Podman rootless
- .dockerignore → .containerignore
- boto3 unpinned from 1.34.0 to >=1.34.0

CI:
- Remove Woodpecker (.woodpecker.yml, .woodpecker/)
- Add Forgejo Actions (.forgejo/workflows/ci.yml, publish.yml)
- CI: syntax check, security scan, container lint (hadolint), build test
- Publish: build and push to Quay.io on main push and version tags

Cleanup:
- Remove entrypoint.sh (no longer needed)
- Remove scripts/build-and-push.sh and PUBLISHING.md (superseded by CI)
- All docker → podman command references updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 15:41:08 -07:00

452 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SGO - Import</title>
<link rel="icon" href="{{ url_for('static', filename='images/logo.svg') }}" type="image/svg+xml">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<style>
.import-container {
max-width: 800px;
margin: 4rem auto;
padding: 2rem;
}
.import-card {
background: var(--card-bg);
border-radius: 0.5rem;
padding: 2rem;
box-shadow: var(--shadow-lg);
border: 1px solid var(--border-color);
}
.import-title {
font-size: 2rem;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 0.5rem;
}
.import-subtitle {
color: var(--text-secondary);
margin-bottom: 2rem;
}
.profile-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
padding: 0.5rem;
margin-bottom: 1rem;
}
.profile-item {
padding: 0.75rem;
border-radius: 0.25rem;
margin-bottom: 0.25rem;
cursor: pointer;
transition: all 0.2s;
}
.profile-item:hover {
background: var(--bg-color);
}
.profile-item label {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
}
.profile-item input[type="checkbox"] {
width: 1.25rem;
height: 1.25rem;
cursor: pointer;
}
.mfa-section {
display: none;
margin-top: 1.5rem;
padding: 1.5rem;
background: var(--bg-color);
border-radius: 0.375rem;
}
.mfa-section.active {
display: block;
}
.mfa-inputs {
display: grid;
gap: 1rem;
}
.mfa-input-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.mfa-input-group label {
font-weight: 600;
color: var(--text-primary);
}
.mfa-input-row {
display: flex;
gap: 0.5rem;
align-items: center;
}
.mfa-input-group input {
flex: 1;
padding: 0.75rem;
border: 2px solid var(--border-color);
border-radius: 0.375rem;
font-size: 1rem;
}
.mfa-input-group input:focus {
outline: none;
border-color: var(--primary-color);
}
.profile-import-btn {
padding: 0.75rem 1.5rem;
background: var(--primary-color);
color: white;
border: none;
border-radius: 0.375rem;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.profile-import-btn:hover:not(:disabled) {
background: var(--primary-hover);
transform: translateY(-1px);
}
.profile-import-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.profile-import-btn.success {
background: var(--success-color);
}
.profile-import-btn.error {
background: var(--danger-color);
}
.import-btn {
width: 100%;
padding: 1rem;
background: var(--primary-color);
color: white;
border: none;
border-radius: 0.5rem;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-top: 1rem;
}
.import-btn:hover:not(:disabled) {
background: var(--primary-hover);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.import-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.select-all-btn {
padding: 0.5rem 1rem;
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875rem;
margin-bottom: 0.5rem;
}
.select-all-btn:hover {
background: var(--border-color);
}
.progress-section {
display: none;
margin-top: 1.5rem;
padding: 1.5rem;
background: var(--bg-color);
border-radius: 0.375rem;
}
.progress-section.active {
display: block;
}
#progressLog {
max-height: 400px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
padding: 0.5rem;
background: var(--bg-color);
}
.progress-item {
padding: 0.5rem 0;
color: var(--text-secondary);
}
.progress-item.success {
color: var(--success-color);
}
.progress-item.error {
color: var(--danger-color);
}
</style>
</head>
<body>
<div class="import-container">
<div class="import-card">
<h1 class="import-title">SG Observatory</h1>
<p class="import-subtitle">Select AWS profiles to import EC2 instances and Security Groups • <a href="https://codeberg.org/edfig/SGO" target="_blank" style="color: inherit; opacity: 0.7; text-decoration: none;">Source Code</a></p>
<div id="loadingProfiles" class="loading">
<div class="spinner"></div>
Loading AWS profiles...
</div>
<div id="profileSelection" style="display: none;">
<button class="select-all-btn" onclick="toggleSelectAll()">Select All / Deselect All</button>
<div class="profile-list" id="profileList"></div>
<div class="mfa-section" id="mfaSection">
<h3>Import Profiles</h3>
<p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem;">
Enter MFA/TOTP codes where required and start imports for selected profiles
</p>
<div class="mfa-inputs" id="mfaInputs"></div>
</div>
<button class="import-btn" id="doneBtn" onclick="goToExplorer()" style="display: none;">
Done - Go to Explorer
</button>
<div class="progress-section" id="progressSection">
<h3>Import Progress</h3>
<div id="progressLog"></div>
</div>
</div>
</div>
</div>
<script>
let profiles = [];
let selectedProfiles = new Set();
let importedProfiles = new Set();
async function loadProfiles() {
try {
const response = await fetch('/api/profiles');
const data = await response.json();
if (data.error) {
document.getElementById('loadingProfiles').innerHTML =
`<div class="empty-state"><div class="empty-state-icon">⚠️</div><p>${data.error}</p></div>`;
return;
}
profiles = data.profiles;
renderProfiles();
document.getElementById('loadingProfiles').style.display = 'none';
document.getElementById('profileSelection').style.display = 'block';
} catch (error) {
document.getElementById('loadingProfiles').innerHTML =
`<div class="empty-state"><div class="empty-state-icon">⚠️</div><p>Error loading profiles: ${error.message}</p></div>`;
}
}
function renderProfiles() {
const list = document.getElementById('profileList');
list.innerHTML = profiles.map((profile, idx) => `
<div class="profile-item">
<label>
<input type="checkbox"
id="profile-${idx}"
value="${profile.name}"
data-has-mfa="${profile.has_mfa}"
onchange="handleProfileSelection()">
<span>${profile.name}</span>
</label>
</div>
`).join('');
}
function toggleSelectAll() {
const checkboxes = document.querySelectorAll('.profile-item input[type="checkbox"]');
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
checkboxes.forEach(cb => cb.checked = !allChecked);
handleProfileSelection();
}
function handleProfileSelection() {
selectedProfiles.clear();
document.querySelectorAll('.profile-item input[type="checkbox"]:checked').forEach(cb => {
selectedProfiles.add({
name: cb.value,
has_mfa: cb.dataset.hasMfa === 'true'
});
});
if (selectedProfiles.size > 0) {
renderMfaInputs();
document.getElementById('mfaSection').classList.add('active');
} else {
document.getElementById('mfaSection').classList.remove('active');
}
}
function renderMfaInputs() {
const container = document.getElementById('mfaInputs');
// Save current MFA values and button states before re-rendering
const savedMfaValues = {};
const savedButtonStates = {};
selectedProfiles.forEach(profile => {
const input = document.getElementById(`mfa-${profile.name}`);
const btn = document.getElementById(`btn-${profile.name}`);
if (input) {
savedMfaValues[profile.name] = input.value;
}
if (btn) {
savedButtonStates[profile.name] = {
text: btn.textContent,
disabled: btn.disabled,
classes: btn.className
};
}
});
// Render all selected profiles, but only show MFA input for profiles that have MFA
container.innerHTML = Array.from(selectedProfiles).map(profile => `
<div class="mfa-input-group">
<label for="mfa-${profile.name}">${profile.name}</label>
<div class="mfa-input-row">
${profile.has_mfa ? `
<input type="text"
id="mfa-${profile.name}"
name="one-time-code"
autocomplete="one-time-code"
inputmode="numeric"
placeholder="Enter MFA/TOTP"
maxlength="6"
pattern="[0-9]*"
onkeydown="if(event.key==='Enter') startProfileImport('${profile.name}')">
` : ''}
<button class="profile-import-btn"
id="btn-${profile.name}"
onclick="startProfileImport('${profile.name}')">
Start Import
</button>
</div>
</div>
`).join('');
// Restore saved values and button states
selectedProfiles.forEach(profile => {
const input = document.getElementById(`mfa-${profile.name}`);
const btn = document.getElementById(`btn-${profile.name}`);
if (savedMfaValues[profile.name] !== undefined) {
input.value = savedMfaValues[profile.name];
}
if (savedButtonStates[profile.name]) {
btn.textContent = savedButtonStates[profile.name].text;
btn.disabled = savedButtonStates[profile.name].disabled;
btn.className = savedButtonStates[profile.name].classes;
}
});
}
async function startProfileImport(profile) {
const btn = document.getElementById(`btn-${profile}`);
const mfaInput = document.getElementById(`mfa-${profile}`);
const progressSection = document.getElementById('progressSection');
const progressLog = document.getElementById('progressLog');
// Disable button and show progress
btn.disabled = true;
btn.textContent = 'Importing...';
btn.className = 'profile-import-btn';
progressSection.classList.add('active');
// Get MFA code for this profile (if MFA input exists)
const mfaCode = mfaInput ? mfaInput.value.trim() : '';
try {
const response = await fetch('/api/import-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
profile: profile,
mfa_code: mfaCode
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let importSuccess = false;
while (true) {
const {value, done} = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split('\n').filter(l => l.trim());
lines.forEach(line => {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.substring(6));
let className = 'progress-item';
if (data.status === 'success') className += ' success';
if (data.status === 'error') className += ' error';
progressLog.innerHTML += `<div class="${className}">${data.message}</div>`;
progressLog.scrollTop = progressLog.scrollHeight;
if (data.status === 'complete') {
importSuccess = true;
importedProfiles.add(profile);
btn.textContent = '✓ Imported';
btn.classList.add('success');
document.getElementById('doneBtn').style.display = 'block';
} else if (data.status === 'error' && data.message.includes('✗')) {
btn.textContent = '✗ Failed';
btn.classList.add('error');
btn.disabled = false;
}
}
});
}
if (!importSuccess && !btn.classList.contains('error')) {
btn.textContent = 'Start Import';
btn.disabled = false;
}
} catch (error) {
progressLog.innerHTML += `<div class="progress-item error">Error: ${error.message}</div>`;
btn.textContent = '✗ Failed';
btn.classList.add('error');
btn.disabled = false;
}
}
function goToExplorer() {
window.location.href = '/explorer';
}
// Load profiles on page load
loadProfiles();
</script>
<footer style="text-align: center; padding: 2rem 1rem; margin-top: 4rem; border-top: 1px solid var(--border-color); color: #64748b; font-size: 0.875rem;">
<p style="margin: 0;">
SGO - Security Groups Observatory •
<a href="https://codeberg.org/edfig/SGO" target="_blank" style="color: var(--primary-color); text-decoration: none;">View Source</a>
<a href="https://codeberg.org/edfig/SGO/issues" target="_blank" style="color: var(--primary-color); text-decoration: none;">Report Issue</a>
</p>
</footer>
</body>
</html>