mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 13:59:40 +00:00
feat(cve-triage): panel tab for generated WAF probes (dry-run + apply)
GET /waf-rules previews kept + rejections (writes nothing); POST /waf-rules/ generate applies (detect mode), refusing on an incomplete presence inventory. Cyan hybrid-skin tab, esc()'d, sbx_token auth. Handlers are plain def (not async def) since gather_present/generate/write_category do blocking work and this module runs in-process inside the aggregator's shared event loop. Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
parent
fedefd36a4
commit
5f459b1de1
|
|
@ -1090,6 +1090,64 @@ async def list_kev_cves():
|
|||
return {"cves": kev_cves, "count": len(kev_cves), "source": "CISA KEV"}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# WAF Product-Absent Probe Generator (wafgen)
|
||||
# ============================================================================
|
||||
# Handlers are plain `def`, not `async def`: gather_present() shells out to
|
||||
# dpkg, generate() does blocking file I/O over the vendored Nuclei subset, and
|
||||
# write_category() is a blocking atomic write. This module is imported
|
||||
# in-process by the secubox aggregator, which runs ONE shared event loop for
|
||||
# ~110 modules — a blocking call inside an `async def` handler would freeze
|
||||
# the whole board. Plain `def` handlers run in FastAPI's threadpool instead.
|
||||
|
||||
@app.get("/waf-rules", dependencies=[Depends(require_jwt)])
|
||||
def waf_rules_preview():
|
||||
"""Dry-run: what would be generated (kept + rejections). Writes nothing."""
|
||||
from .wafgen.generate import generate
|
||||
from .wafgen.inventory import gather_present
|
||||
|
||||
present, complete = gather_present()
|
||||
kept, rejected = generate(
|
||||
Path("/usr/lib/secubox/cve-triage/nuclei-subset"), present, complete
|
||||
)
|
||||
return {
|
||||
"present_count": len(present),
|
||||
"inventory_complete": complete,
|
||||
"kept": [
|
||||
{"cve": c.cve, "vendor": c.vendor, "product": c.product, "path": c.path}
|
||||
for c in kept
|
||||
],
|
||||
"rejected": [{"file": n, "reason": r} for n, r in rejected],
|
||||
}
|
||||
|
||||
|
||||
@app.post("/waf-rules/generate", dependencies=[Depends(require_jwt)])
|
||||
def waf_rules_generate():
|
||||
"""Apply: write product_absent_probes (detect mode). Refuses if the
|
||||
presence inventory is incomplete (fail-safe)."""
|
||||
from .wafgen.generate import generate
|
||||
from .wafgen.inventory import gather_present
|
||||
from .wafgen.emit import write_category
|
||||
|
||||
present, complete = gather_present()
|
||||
if not complete:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="presence inventory incomplete — refusing (fail-safe)",
|
||||
)
|
||||
kept, rejected = generate(
|
||||
Path("/usr/lib/secubox/cve-triage/nuclei-subset"), present, complete
|
||||
)
|
||||
now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
write_category(Path("/etc/secubox/waf/waf-rules.json"), kept, now=now)
|
||||
return {
|
||||
"success": True,
|
||||
"written": len(kept),
|
||||
"mode": "detect",
|
||||
"rejected": len(rejected),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Startup
|
||||
# ============================================================================
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@
|
|||
<button class="tab" onclick="showTab('recent')">Recent (7d)</button>
|
||||
<button class="tab" onclick="showTab('triage')">My Triage</button>
|
||||
<button class="tab" onclick="showTab('search')">Search</button>
|
||||
<button class="tab" onclick="showTab('waf')">WAF Probes</button>
|
||||
</div>
|
||||
|
||||
<!-- CISA KEV Tab -->
|
||||
|
|
@ -229,6 +230,31 @@
|
|||
<tbody id="searchTable"><tr><td colspan="5" style="text-align:center;color:var(--p31-dim);padding:2rem;">Enter a search term above</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- WAF Probes Tab -->
|
||||
<div class="tab-content" id="tab-waf">
|
||||
<p style="color:var(--p31-dim);font-size:0.75rem;margin-bottom:0.75rem;">
|
||||
Product-absent probe candidates generated from the vendored Nuclei KEV subset.
|
||||
This is a dry-run preview — nothing is written until you click Generate.
|
||||
Generation refuses (409) if the presence inventory is incomplete (fail-safe).
|
||||
</p>
|
||||
<div class="form-row" style="align-items:center;">
|
||||
<div style="font-size:0.75rem;">Present products: <strong id="wafPresentCount">-</strong></div>
|
||||
<div style="font-size:0.75rem;">Inventory: <strong id="wafInventoryComplete">-</strong></div>
|
||||
<button class="btn primary" id="wafGenerateBtn" onclick="generateWafRules()">Generate</button>
|
||||
<button class="btn" onclick="loadWafRules()">Refresh</button>
|
||||
</div>
|
||||
<h2 style="font-size:0.8rem;margin-top:0.75rem;">Kept <span class="count" id="wafKeptCount">0</span></h2>
|
||||
<table>
|
||||
<thead><tr><th>CVE</th><th>Vendor</th><th>Path</th></tr></thead>
|
||||
<tbody id="wafKeptTable"><tr><td colspan="3" class="loading"><span class="spinner"></span></td></tr></tbody>
|
||||
</table>
|
||||
<h2 style="font-size:0.8rem;margin-top:0.75rem;">Rejected <span class="count" id="wafRejectedCount">0</span></h2>
|
||||
<table>
|
||||
<thead><tr><th>File</th><th>Reason</th></tr></thead>
|
||||
<tbody id="wafRejectedTable"><tr><td colspan="2" class="loading"><span class="spinner"></span></td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Package Scan Card -->
|
||||
|
|
@ -258,6 +284,10 @@
|
|||
const token = () => localStorage.getItem('sbx_token');
|
||||
const headers = () => ({ 'Content-Type': 'application/json', ...(token() ? { 'Authorization': 'Bearer ' + token() } : {}) });
|
||||
|
||||
function esc(s) {
|
||||
return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
}
|
||||
|
||||
async function api(path, opts = {}) {
|
||||
try {
|
||||
const res = await fetch(API + path, { ...opts, headers: headers() });
|
||||
|
|
@ -280,6 +310,7 @@
|
|||
if (name === 'trending') loadTrending();
|
||||
if (name === 'recent') loadRecent();
|
||||
if (name === 'triage') loadTriage();
|
||||
if (name === 'waf') loadWafRules();
|
||||
}
|
||||
|
||||
// Stats & Feed Summary
|
||||
|
|
@ -577,6 +608,60 @@
|
|||
refresh();
|
||||
}
|
||||
|
||||
// WAF Probes (product-absent probe generator, dry-run preview + apply)
|
||||
async function loadWafRules() {
|
||||
const keptBody = document.getElementById('wafKeptTable');
|
||||
const rejBody = document.getElementById('wafRejectedTable');
|
||||
keptBody.innerHTML = '<tr><td colspan="3" class="loading"><span class="spinner"></span></td></tr>';
|
||||
rejBody.innerHTML = '<tr><td colspan="2" class="loading"><span class="spinner"></span></td></tr>';
|
||||
|
||||
const d = await api('/waf-rules');
|
||||
|
||||
document.getElementById('wafPresentCount').textContent = d.present_count ?? '-';
|
||||
document.getElementById('wafInventoryComplete').textContent =
|
||||
d.inventory_complete === undefined ? '-' : (d.inventory_complete ? 'complete' : 'INCOMPLETE (fail-safe)');
|
||||
|
||||
const kept = d.kept || [];
|
||||
document.getElementById('wafKeptCount').textContent = kept.length;
|
||||
keptBody.innerHTML = kept.length ? kept.map(c => `
|
||||
<tr>
|
||||
<td><code>${esc(c.cve)}</code></td>
|
||||
<td>${esc(c.vendor)}</td>
|
||||
<td><code>${esc(c.path)}</code></td>
|
||||
</tr>
|
||||
`).join('') : '<tr><td colspan="3" style="text-align:center;color:var(--p31-dim);padding:2rem;">No candidates kept</td></tr>';
|
||||
|
||||
const rejected = d.rejected || [];
|
||||
document.getElementById('wafRejectedCount').textContent = rejected.length;
|
||||
rejBody.innerHTML = rejected.length ? rejected.map(r => `
|
||||
<tr>
|
||||
<td><code>${esc(r.file)}</code></td>
|
||||
<td>${esc(r.reason)}</td>
|
||||
</tr>
|
||||
`).join('') : '<tr><td colspan="2" style="text-align:center;color:var(--p31-dim);padding:2rem;">No rejections</td></tr>';
|
||||
}
|
||||
|
||||
async function generateWafRules() {
|
||||
const btn = document.getElementById('wafGenerateBtn');
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const res = await fetch(API + '/waf-rules/generate', { method: 'POST', headers: headers() });
|
||||
if (res.status === 401) { window.location = '/login.html'; return; }
|
||||
const text = await res.text();
|
||||
const body = text ? JSON.parse(text) : {};
|
||||
if (!res.ok) {
|
||||
alert(`Generate failed: ${body.detail || res.status}`);
|
||||
return;
|
||||
}
|
||||
alert(`Written ${body.written || 0} probe(s) (mode=detect), ${body.rejected || 0} rejected`);
|
||||
loadWafRules();
|
||||
} catch (e) {
|
||||
alert('Generate failed: request error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh
|
||||
function refresh() {
|
||||
loadStats();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user