update webapp for isp recognition

This commit is contained in:
Purple
2026-01-17 21:44:48 +00:00
parent e6f8662052
commit 54307c7b73
4 changed files with 189 additions and 5 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -127,3 +127,43 @@ INSERT INTO geofeed_settings (setting_key, setting_value) VALUES
('ipregistry_api_key', ''),
('ipregistry_enabled', '0')
ON DUPLICATE KEY UPDATE setting_key = setting_key;
-- ============================================
-- MIGRATION: Add columns for existing databases
-- These statements safely add columns if they don't exist
-- ============================================
-- Add sort_order column
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS sort_order INT DEFAULT 0 AFTER notes;
-- Add IP Registry enrichment columns
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS ipr_enriched_at TIMESTAMP NULL DEFAULT NULL AFTER sort_order;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS ipr_isp VARCHAR(255) DEFAULT NULL AFTER ipr_enriched_at;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS ipr_org VARCHAR(255) DEFAULT NULL AFTER ipr_isp;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS ipr_asn INT DEFAULT NULL AFTER ipr_org;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS ipr_asn_name VARCHAR(255) DEFAULT NULL AFTER ipr_asn;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS ipr_connection_type VARCHAR(50) DEFAULT NULL AFTER ipr_asn_name;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS ipr_country_name VARCHAR(100) DEFAULT NULL AFTER ipr_connection_type;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS ipr_region_name VARCHAR(100) DEFAULT NULL AFTER ipr_country_name;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS ipr_timezone VARCHAR(100) DEFAULT NULL AFTER ipr_region_name;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS ipr_latitude DECIMAL(10, 7) DEFAULT NULL AFTER ipr_timezone;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS ipr_longitude DECIMAL(10, 7) DEFAULT NULL AFTER ipr_latitude;
-- Add security flag columns
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS flag_abuser TINYINT(1) DEFAULT 0 AFTER ipr_longitude;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS flag_attacker TINYINT(1) DEFAULT 0 AFTER flag_abuser;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS flag_bogon TINYINT(1) DEFAULT 0 AFTER flag_attacker;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS flag_cloud_provider TINYINT(1) DEFAULT 0 AFTER flag_bogon;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS flag_proxy TINYINT(1) DEFAULT 0 AFTER flag_cloud_provider;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS flag_relay TINYINT(1) DEFAULT 0 AFTER flag_proxy;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS flag_tor TINYINT(1) DEFAULT 0 AFTER flag_relay;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS flag_tor_exit TINYINT(1) DEFAULT 0 AFTER flag_tor;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS flag_vpn TINYINT(1) DEFAULT 0 AFTER flag_tor_exit;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS flag_anonymous TINYINT(1) DEFAULT 0 AFTER flag_vpn;
ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS flag_threat TINYINT(1) DEFAULT 0 AFTER flag_anonymous;
-- Add indexes if they don't exist (MariaDB 10.5+ supports IF NOT EXISTS for indexes)
-- For older versions, these will show warnings but won't fail
ALTER TABLE geofeed_entries ADD INDEX IF NOT EXISTS idx_sort_order (sort_order);
ALTER TABLE geofeed_entries ADD INDEX IF NOT EXISTS idx_isp (ipr_isp);
ALTER TABLE geofeed_entries ADD INDEX IF NOT EXISTS idx_asn (ipr_asn);

View File

@@ -391,8 +391,12 @@ function requireAuthApi() {
/**
* Fetch IP data from ipregistry.co
*/
function fetchIpRegistryData($ipPrefix) {
function fetchIpRegistryData($ipPrefix, $db = null) {
// Try environment variable first, then database setting
$apiKey = IPREGISTRY_API_KEY;
if (empty($apiKey) && $db !== null) {
$apiKey = getSetting($db, 'ipregistry_api_key', '');
}
if (empty($apiKey)) {
return ['success' => false, 'error' => 'IP Registry API key not configured'];
}
@@ -439,11 +443,12 @@ function fetchIpRegistryData($ipPrefix) {
}
// Extract relevant fields
// Note: ipregistry.co doesn't have a direct 'isp' field - organization is the ISP/org name
return [
'success' => true,
'data' => [
'ipr_isp' => $data['connection']['isp'] ?? null,
'ipr_org' => $data['connection']['organization'] ?? null,
'ipr_isp' => $data['connection']['organization'] ?? null,
'ipr_org' => $data['company']['name'] ?? $data['connection']['organization'] ?? null,
'ipr_asn' => $data['connection']['asn'] ?? null,
'ipr_asn_name' => $data['connection']['domain'] ?? null,
'ipr_connection_type' => $data['connection']['type'] ?? null,
@@ -472,7 +477,7 @@ function fetchIpRegistryData($ipPrefix) {
* Enrich IP entry with IP Registry data
*/
function enrichIpEntry($db, $entryId, $ipPrefix) {
$result = fetchIpRegistryData($ipPrefix);
$result = fetchIpRegistryData($ipPrefix, $db);
if (!$result['success']) {
return $result;

View File

@@ -1941,6 +1941,30 @@ if (function_exists('requireAuth')) {
<label class="form-label">Notes</label>
<input type="text" class="form-input" id="notes" name="notes" placeholder="Optional notes for internal use">
</div>
<!-- IP Enrichment Section (shown only when editing) -->
<div id="enrichmentSection" style="display: none; margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border-color);">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
<label class="form-label" style="margin-bottom: 0;">IP Registry Enrichment</label>
<button type="button" class="btn btn-secondary btn-sm" id="reEnrichBtn" onclick="reEnrichCurrentEntry()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M23 4v6h-6M1 20v-6h6"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>
<span id="reEnrichBtnText">Re-enrich IP</span>
</button>
</div>
<div id="enrichmentStatus" class="form-hint" style="font-size: 12px;"></div>
<div id="enrichmentData" style="display: none; margin-top: 8px; font-size: 12px; color: var(--text-secondary);">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 4px 16px;">
<div><strong>ISP:</strong> <span id="enrichIsp">-</span></div>
<div><strong>ASN:</strong> <span id="enrichAsn">-</span></div>
<div><strong>Org:</strong> <span id="enrichOrg">-</span></div>
<div><strong>Type:</strong> <span id="enrichType">-</span></div>
</div>
<div id="enrichFlags" style="margin-top: 8px;"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
@@ -2680,6 +2704,9 @@ if (function_exists('requireAuth')) {
const modal = document.getElementById('entryModal');
const form = document.getElementById('entryForm');
const title = document.getElementById('modalTitle');
const enrichSection = document.getElementById('enrichmentSection');
const enrichStatus = document.getElementById('enrichmentStatus');
const enrichData = document.getElementById('enrichmentData');
form.reset();
@@ -2693,9 +2720,53 @@ if (function_exists('requireAuth')) {
document.getElementById('postalCode').value = entry.postal_code || '';
document.getElementById('clientShortName').value = entry.client_short_name || '';
document.getElementById('notes').value = entry.notes || '';
// Show enrichment section when editing
enrichSection.style.display = 'block';
if (entry.ipr_enriched_at) {
const enrichedDate = new Date(entry.ipr_enriched_at).toLocaleString();
enrichStatus.innerHTML = `<span style="color: var(--success);">Enriched on ${enrichedDate}</span>`;
enrichData.style.display = 'block';
// Populate enrichment data
document.getElementById('enrichIsp').textContent = entry.ipr_isp || '-';
document.getElementById('enrichAsn').textContent = entry.ipr_asn ? `AS${entry.ipr_asn}` : '-';
document.getElementById('enrichOrg').textContent = entry.ipr_org || '-';
document.getElementById('enrichType').textContent = entry.ipr_connection_type || '-';
// Show active flags
const flags = [];
if (entry.flag_abuser == 1) flags.push('Abuser');
if (entry.flag_attacker == 1) flags.push('Attacker');
if (entry.flag_bogon == 1) flags.push('Bogon');
if (entry.flag_cloud_provider == 1) flags.push('Cloud');
if (entry.flag_proxy == 1) flags.push('Proxy');
if (entry.flag_relay == 1) flags.push('Relay');
if (entry.flag_tor == 1) flags.push('Tor');
if (entry.flag_tor_exit == 1) flags.push('Tor Exit');
if (entry.flag_vpn == 1) flags.push('VPN');
if (entry.flag_anonymous == 1) flags.push('Anonymous');
if (entry.flag_threat == 1) flags.push('Threat');
const flagsEl = document.getElementById('enrichFlags');
if (flags.length > 0) {
flagsEl.innerHTML = '<strong>Flags:</strong> ' + flags.map(f => `<span style="background: var(--danger); color: white; padding: 1px 6px; border-radius: 4px; font-size: 11px; margin-left: 4px;">${f}</span>`).join('');
} else {
flagsEl.innerHTML = '<strong>Flags:</strong> <span style="color: var(--success);">None</span>';
}
} else {
enrichStatus.innerHTML = '<span style="color: var(--text-tertiary);">Not enriched yet</span>';
enrichData.style.display = 'none';
}
// Reset button state
document.getElementById('reEnrichBtn').disabled = false;
document.getElementById('reEnrichBtnText').textContent = 'Re-enrich IP';
} else {
title.textContent = 'Add Entry';
document.getElementById('entryId').value = '';
enrichSection.style.display = 'none';
}
modal.classList.add('active');
@@ -3062,7 +3133,7 @@ if (function_exists('requireAuth')) {
try {
const result = await api('enrich_all', {}, 'POST', {});
if (result.success) {
showToast(`Enriched ${result.enriched} IPs. ${result.pending_enrichment || 0} remaining.`, 'success');
showToast(`Enriched ${result.enriched} IPs. ${result.failed || 0} failed.`, 'success');
loadEntries(currentPage);
} else {
showToast(result.error || 'Failed to enrich IPs', 'error');
@@ -3075,6 +3146,74 @@ if (function_exists('requireAuth')) {
}
}
// Re-enrich current entry from edit modal
async function reEnrichCurrentEntry() {
const id = document.getElementById('entryId').value;
if (!id) return;
const btn = document.getElementById('reEnrichBtn');
const btnText = document.getElementById('reEnrichBtnText');
btn.disabled = true;
btnText.textContent = 'Enriching...';
try {
const result = await api('enrich_ip', {}, 'POST', { id: parseInt(id) });
if (result.success) {
showToast('IP enriched successfully', 'success');
// Reload the entry data to update the modal
const entryResult = await api('get', { id });
if (entryResult.success) {
// Update enrichment display without closing modal
const entry = entryResult.data;
const enrichStatus = document.getElementById('enrichmentStatus');
const enrichData = document.getElementById('enrichmentData');
const enrichedDate = new Date(entry.ipr_enriched_at).toLocaleString();
enrichStatus.innerHTML = `<span style="color: var(--success);">Enriched on ${enrichedDate}</span>`;
enrichData.style.display = 'block';
document.getElementById('enrichIsp').textContent = entry.ipr_isp || '-';
document.getElementById('enrichAsn').textContent = entry.ipr_asn ? `AS${entry.ipr_asn}` : '-';
document.getElementById('enrichOrg').textContent = entry.ipr_org || '-';
document.getElementById('enrichType').textContent = entry.ipr_connection_type || '-';
// Update flags
const flags = [];
if (entry.flag_abuser == 1) flags.push('Abuser');
if (entry.flag_attacker == 1) flags.push('Attacker');
if (entry.flag_bogon == 1) flags.push('Bogon');
if (entry.flag_cloud_provider == 1) flags.push('Cloud');
if (entry.flag_proxy == 1) flags.push('Proxy');
if (entry.flag_relay == 1) flags.push('Relay');
if (entry.flag_tor == 1) flags.push('Tor');
if (entry.flag_tor_exit == 1) flags.push('Tor Exit');
if (entry.flag_vpn == 1) flags.push('VPN');
if (entry.flag_anonymous == 1) flags.push('Anonymous');
if (entry.flag_threat == 1) flags.push('Threat');
const flagsEl = document.getElementById('enrichFlags');
if (flags.length > 0) {
flagsEl.innerHTML = '<strong>Flags:</strong> ' + flags.map(f => `<span style="background: var(--danger); color: white; padding: 1px 6px; border-radius: 4px; font-size: 11px; margin-left: 4px;">${f}</span>`).join('');
} else {
flagsEl.innerHTML = '<strong>Flags:</strong> <span style="color: var(--success);">None</span>';
}
}
// Also refresh the main table
loadEntries(currentPage);
} else {
showToast(result.error || 'Failed to enrich IP', 'error');
}
} catch (error) {
showToast('Network error', 'error');
} finally {
btn.disabled = false;
btnText.textContent = 'Re-enrich IP';
}
}
// Logout function
async function logout() {
try {