Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,55 @@ jobs:
# Same script developers run locally via ./tools/run-tests.sh, so green here == green there.
# shellcheck is pre-installed on the ubuntu runners.
run: PYTHON=python bash tools/run-tests.sh

coverage:
name: coverage
runs-on: ubuntu-latest
# Informational, and deliberately a SEPARATE job: it re-runs the suites under tracing, which is
# slower, and the gate above should stay the fast answer. Codacy has a 60% coverage goal that
# has always read as "not reported" because nothing ever produced a report.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"

- name: Install dependencies
run: python -m pip install --quiet -r requirements.txt coverage

- name: Measure
# --parallel-mode + combine: each suite is its own process, and smoke/rbac fork threads.
# || true on the suites themselves — the gate job decides pass/fail, this one only measures.
run: |
for suite in unit template_actions smoke rbac; do
rm -f data/panel.db data/panel.db-shm data/panel.db-wal data/panel.db.backup
python -m coverage run --parallel-mode --source=. \
--omit="./tests/*,./tools/*,./.venv/*" "tests/${suite}_test.py" >/dev/null 2>&1 || true
done
python -m coverage combine
python -m coverage xml -o coverage.xml
python -m coverage report --sort=miss > coverage.txt
{
echo '### Coverage'
echo ''
echo '```'
tail -n 20 coverage.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload the report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: coverage
path: |
coverage.xml
coverage.txt

# Codacy's coverage goal only lights up once a report is uploaded, which needs a project
# token this repo does not have. To enable: add CODACY_PROJECT_TOKEN as a repository secret
# and uncomment. Left off rather than half-wired, so the job never fails on a missing secret.
# - name: Send to Codacy
# env:
# CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
# run: bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r coverage.xml
509 changes: 509 additions & 0 deletions static/js/dashboard.js

Large diffs are not rendered by default.

611 changes: 611 additions & 0 deletions static/js/manage_remotes.js

Large diffs are not rendered by default.

308 changes: 308 additions & 0 deletions static/js/manage_servers.js

Large diffs are not rendered by default.

191 changes: 191 additions & 0 deletions static/js/remote_firewall.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
function refreshFirewall() {
fetch(MOUNT + '/api/remote/' + remoteId + '/firewall')
.then(r => r.json())
.then(data => {
var badge = document.getElementById('ufw-badge');
badge.textContent = data.enabled ? 'Active' : 'Inactive';
badge.className = 'badge ' + (data.enabled ? 'bg-success' : 'bg-secondary');

var listEl = document.getElementById('rules-list');
var groups = data.groups || [];
var openGroups = groups.filter(function(g) { return !g.is_block; });
var blockGroups = groups.filter(function(g) { return g.is_block; });
if (openGroups.length) {
var html = '<table class="table table-sm table-hover mb-0 align-middle">'
+ '<thead><tr><th>Port</th><th>Protocol</th><th>For</th><th>Scope</th><th>IP</th><th></th></tr></thead><tbody>';
openGroups.forEach(function(g) {
var port = g.is_iface
? '<span class="badge bg-info text-dark">' + esc(g.port_num) + '</span>'
: '<code>' + esc(g.port_num) + '</code>';
var scope = (g.action !== 'ALLOW' ? '<span class="badge bg-danger me-1">' + esc(g.action) + '</span>' : '') + esc(g.scope);
html += '<tr><td>' + port + '</td>'
+ '<td>' + protoBadge(g.proto_label) + '</td>'
+ '<td class="small">' + esc(g.comment || '—') + '</td>'
+ '<td class="small text-secondary">' + scope + '</td>'
+ '<td><span class="text-secondary" style="font-size:.68rem;">' + esc(g.family_label) + '</span></td>'
+ '<td>' + (g.protected
? '<button class="btn btn-outline-secondary btn-sm py-0 px-1" disabled title="' + esc(g.protect_reason) + '"><i class="bi bi-lock-fill"></i></button>'
: '<button class="btn btn-outline-danger btn-sm py-0 px-1"' + _da('deleteGroup', [g.nums, '@self', !!g.warn, (g.protect_reason || '')]) + '><i class="bi bi-x"></i></button>')
+ '</td></tr>';
});
html += '</tbody></table>';
listEl.innerHTML = html;

Check failure on line 32 in static/js/remote_firewall.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

static/js/remote_firewall.js#L32

User controlled data in a `listEl.innerHTML` is an anti-pattern that can lead to XSS vulnerabilities

Check failure on line 32 in static/js/remote_firewall.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

static/js/remote_firewall.js#L32

User controlled data in methods like `innerHTML`, `outerHTML` or `document.write` is an anti-pattern that can lead to XSS vulnerabilities
} else {
listEl.innerHTML = '<div class="p-3 text-center text-secondary small">No open ports yet.</div>';
}
document.getElementById('rules-count').textContent = openGroups.length + (openGroups.length === 1 ? ' rule' : ' rules');

// Blocked IPs (separate card)
var blocksEl = document.getElementById('blocks-list');
if (blocksEl) {
if (blockGroups.length) {
var bh = '<table class="table table-sm table-hover mb-0 align-middle">'
+ '<thead><tr><th>IP address</th><th>Source</th><th>Family</th><th></th></tr></thead><tbody>';
blockGroups.forEach(function(g) {
bh += '<tr><td><code>' + esc(g.block_ip) + '</code></td>'
+ '<td>' + blockBadge(g.comment) + '</td>'
+ '<td><span class="text-secondary" style="font-size:.68rem;">' + esc(g.family_label) + '</span></td>'
+ '<td><button class="btn btn-outline-warning btn-sm py-0 px-1" title="Unblock ' + esc(g.block_ip) + '"'
+ _da('unblockIp', [g.block_ip, '@self']) + '><i class="bi bi-x"></i></button></td></tr>';
});
bh += '</tbody></table>';
blocksEl.innerHTML = bh;

Check failure on line 52 in static/js/remote_firewall.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

static/js/remote_firewall.js#L52

User controlled data in a `blocksEl.innerHTML` is an anti-pattern that can lead to XSS vulnerabilities

Check failure on line 52 in static/js/remote_firewall.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

static/js/remote_firewall.js#L52

User controlled data in methods like `innerHTML`, `outerHTML` or `document.write` is an anti-pattern that can lead to XSS vulnerabilities
} else {
blocksEl.innerHTML = '<div class="p-3 text-center text-secondary small">No IPs are blocked.</div>';
}
var bc = document.getElementById('blocks-count');
if (bc) bc.textContent = blockGroups.length + ' blocked';
}
});
}

function blockBadge(c) {
if (c === 'panel-autoblock') return '<span class="badge bg-info text-dark">Auto</span>';
if (c === 'panel-block') return '<span class="badge bg-secondary">Manual</span>';
return c ? '<span class="badge bg-dark">' + esc(c) + '</span>' : '<span class="text-secondary">—</span>';
}

function blockIp() {
var ip = document.getElementById('block-ip').value.trim();
if (!ip) return;
var resultEl = document.getElementById('block-result');
resultEl.innerHTML = '<span class="text-secondary"><i class="bi bi-arrow-repeat"></i> Blocking…</span>';
fetch(MOUNT + '/api/remote/' + remoteId + '/security/block', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ip: ip}),
})
.then(r => r.json())
.then(data => {
if (data.success) {
resultEl.innerHTML = '<span class="text-success">✅ ' + esc(data.message) + '</span>';
document.getElementById('block-ip').value = '';
refreshFirewall();
} else {
resultEl.innerHTML = '<span class="text-danger">❌ ' + esc(data.message) + '</span>';

Check failure on line 85 in static/js/remote_firewall.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

static/js/remote_firewall.js#L85

User controlled data in methods like `innerHTML`, `outerHTML` or `document.write` is an anti-pattern that can lead to XSS vulnerabilities
}
})
.catch(function() { resultEl.innerHTML = '<span class="text-danger">❌ Request failed</span>'; });
}

function unblockIp(ip, btn) {
confirmDialog({title: 'Unblock IP', icon: 'shield-check', confirmClass: 'btn-warning', confirmLabel: 'Unblock',
bodyText: 'Remove the firewall block on ' + ip + '?',
onConfirm: function() {
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>'; }
fetch(MOUNT + '/api/remote/' + remoteId + '/security/block', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ip: ip, unblock: true}),
})
.then(r => r.json())
.then(function(d) { if (!d.success && window.toast) toast(d.message || 'Failed to unblock', 'danger'); refreshFirewall(); })
.catch(function() { refreshFirewall(); });
}});
}

function esc(s){ return window.escapeHtml(s); }

function protoBadge(p) {
if (p === 'TCP') return '<span class="badge bg-primary">TCP</span>';
if (p === 'UDP') return '<span class="badge bg-warning text-dark">UDP</span>';
if (p === 'BOTH') return '<span class="badge bg-secondary">Both</span>';
return '<span class="text-secondary">—</span>';
}

function openPort() {
var port = document.getElementById('new-port').value.trim();
var proto = document.getElementById('new-proto').value;
var comment = document.getElementById('new-comment').value.trim();
if (!port) return;
var resultEl = document.getElementById('port-result');
resultEl.innerHTML = '<span class="text-secondary"><i class="bi bi-arrow-repeat"></i> Opening...</span>';

fetch(MOUNT + '/api/remote/' + remoteId + '/firewall/open', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({port: parseInt(port), protocol: proto, comment: comment}),
})
.then(r => r.json())
.then(data => {
if (data.success) {
resultEl.innerHTML = '<span class="text-success">✅ ' + esc(data.message) + '</span>';
document.getElementById('new-port').value = '';
document.getElementById('new-comment').value = '';
refreshFirewall();
} else {
resultEl.innerHTML = '<span class="text-danger">❌ ' + esc(data.message) + '</span>';
}
});
}

// Delete a whole rule group (its IPv4 + IPv6 entries). UFW renumbers rules above a
// deleted one, so delete highest-number-first to keep the remaining indices valid.
function deleteGroup(nums, btn, warn, reason) {
if (!nums || !nums.length) return;
var msg = warn ? ((reason || 'This may affect your access.') + '\n\nRemove this rule anyway?')
: 'Remove this firewall rule?';
confirmDialog({title:'Remove firewall rule', icon:'shield-exclamation',
confirmClass: warn ? 'btn-danger' : 'btn-warning', confirmLabel:'Remove', bodyText: msg,
onConfirm: function(){
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>'; }
var ordered = nums.slice().sort(function(a, b) { return b - a; });
(function next(i) {
if (i >= ordered.length) { refreshFirewall(); return; }
fetch(MOUNT + '/api/remote/' + remoteId + '/firewall/delete-rule', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({num: ordered[i]}),
})
.then(r => r.json())
.then(function(d) { if (!d.success && window.toast) toast(d.message || 'Failed to delete rule', 'danger'); next(i + 1); })
.catch(function() { next(i + 1); });
})(0);
}});
}

function syncPorts(serverId, btn) {
var orig = btn.innerHTML;
btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Detecting…';
fetch(MOUNT + '/api/server/' + serverId + '/sync-ports', { method: 'POST' })

Check failure on line 170 in static/js/remote_firewall.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

static/js/remote_firewall.js#L170

This application allows user-controlled URLs to be passed directly to HTTP client libraries.
.then(r => r.json())
.then(data => {
if (data.success) { if(window.toast) toast(data.message, 'success'); refreshFirewall(); }
else if(window.toast) { toast(data.message || 'Failed', 'danger'); }
})
.catch(function(){ if(window.toast) toast('Request failed', 'danger'); })
.finally(function(){ btn.disabled = false; btn.innerHTML = orig; });

Check failure on line 177 in static/js/remote_firewall.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

static/js/remote_firewall.js#L177

User controlled data in a `btn.innerHTML` is an anti-pattern that can lead to XSS vulnerabilities

Check failure on line 177 in static/js/remote_firewall.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

static/js/remote_firewall.js#L177

User controlled data in methods like `innerHTML`, `outerHTML` or `document.write` is an anti-pattern that can lead to XSS vulnerabilities
}

// Enter key opens port
document.getElementById('new-port').addEventListener('keydown', function(e) {
if (e.key === 'Enter') openPort();
});

// Enter key blocks the typed IP
var blockIpInput = document.getElementById('block-ip');
if (blockIpInput) {
blockIpInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') blockIp();
});
}
Loading