|
5 | 5 |
|
6 | 6 | (function () { |
7 | 7 | const fileInput = document.getElementById('fileInput'); |
8 | | - const eventTypeInput = document.getElementById('eventTypeInput'); |
9 | 8 | const ipMapInput = document.getElementById('ipMapInput'); |
10 | 9 | const statusEl = document.getElementById('status'); |
11 | 10 | const svg = d3.select('#chart'); |
|
76 | 75 | const text = await file.text(); |
77 | 76 | const rows = d3.csvParse(text.trim()); |
78 | 77 | lastRawCsvRows = rows; // cache raw rows |
| 78 | + |
| 79 | + console.log('Processing CSV with IP map status:', { |
| 80 | + ipMapLoaded, |
| 81 | + ipMapSize: ipIdToAddr ? ipIdToAddr.size : 0 |
| 82 | + }); |
| 83 | + |
| 84 | + // Warn if IP map is not loaded |
| 85 | + if (!ipMapLoaded || !ipIdToAddr || ipIdToAddr.size === 0) { |
| 86 | + console.warn('IP map not loaded or empty. Some IP IDs may not be mapped correctly.'); |
| 87 | + status('Warning: IP map not loaded. Some data may be filtered out.'); |
| 88 | + } |
| 89 | + |
79 | 90 | const data = rows.map((d, i) => { |
80 | 91 | const attackName = decodeAttack(d.attack); |
81 | 92 | const attackGroupName = decodeAttackGroup(d.attack_group, d.attack); |
| 93 | + const srcIp = decodeIp(d.src_ip); |
| 94 | + const dstIp = decodeIp(d.dst_ip); |
82 | 95 | return { |
83 | 96 | idx: i, |
84 | 97 | timestamp: toNumber(d.timestamp), |
85 | 98 | length: toNumber(d.length), |
86 | | - src_ip: decodeIp(d.src_ip), |
87 | | - dst_ip: decodeIp(d.dst_ip), |
| 99 | + src_ip: srcIp, |
| 100 | + dst_ip: dstIp, |
88 | 101 | protocol: (d.protocol || '').toUpperCase() || 'OTHER', |
89 | 102 | count: toNumber(d.count) || 1, |
90 | 103 | attack: attackName, |
91 | 104 | attack_group: attackGroupName, |
92 | 105 | }; |
93 | | - }).filter(d => isFinite(d.timestamp) && d.src_ip && d.dst_ip); |
| 106 | + }).filter(d => { |
| 107 | + // Filter out records with invalid data |
| 108 | + const hasValidTimestamp = isFinite(d.timestamp); |
| 109 | + const hasValidSrcIp = d.src_ip && d.src_ip !== 'N/A' && !d.src_ip.startsWith('IP_'); |
| 110 | + const hasValidDstIp = d.dst_ip && d.dst_ip !== 'N/A' && !d.dst_ip.startsWith('IP_'); |
| 111 | + |
| 112 | + // Debug logging for filtered records |
| 113 | + if (!hasValidSrcIp || !hasValidDstIp) { |
| 114 | + console.log('Filtering out record:', { |
| 115 | + src_ip: d.src_ip, |
| 116 | + dst_ip: d.dst_ip, |
| 117 | + hasValidSrcIp, |
| 118 | + hasValidDstIp, |
| 119 | + ipMapLoaded, |
| 120 | + ipMapSize: ipIdToAddr ? ipIdToAddr.size : 0 |
| 121 | + }); |
| 122 | + } |
| 123 | + |
| 124 | + return hasValidTimestamp && hasValidSrcIp && hasValidDstIp; |
| 125 | + }); |
94 | 126 |
|
95 | 127 | if (data.length === 0) { |
96 | | - status('No valid rows found. Ensure CSV has required columns.'); |
| 128 | + status('No valid rows found. Ensure CSV has required columns and IP mappings are available.'); |
97 | 129 | clearChart(); |
98 | 130 | return; |
99 | 131 | } |
| 132 | + |
| 133 | + // Report how many rows were filtered out |
| 134 | + const totalRows = rows.length; |
| 135 | + const filteredRows = totalRows - data.length; |
| 136 | + if (filteredRows > 0) { |
| 137 | + status(`Loaded ${data.length} valid rows (${filteredRows} rows filtered due to missing IP mappings)`); |
| 138 | + } else { |
| 139 | + status(`Loaded ${data.length} records`); |
| 140 | + } |
| 141 | + |
100 | 142 | render(data); |
101 | 143 | } catch (err) { |
102 | 144 | console.error(err); |
|
105 | 147 | } |
106 | 148 | }); |
107 | 149 |
|
108 | | - // Allow user to upload a custom event_type_mapping JSON to override default mapping |
109 | | - eventTypeInput?.addEventListener('change', async (e) => { |
110 | | - const file = e.target.files?.[0]; |
111 | | - if (!file) return; |
112 | | - status(`Loading event type map ${file.name} …`); |
113 | | - try { |
114 | | - const text = await file.text(); |
115 | | - const obj = JSON.parse(text); |
116 | | - // Expect format name -> id OR id -> name. Detect by sampling a few keys. |
117 | | - const entries = Object.entries(obj); |
118 | | - const rev = new Map(); |
119 | | - if (entries.length) { |
120 | | - // Heuristic: if value is number, we assume name->id and reverse; else if key is numeric, we assume id->name. |
121 | | - let nameToIdMode = 0, idToNameMode = 0; |
122 | | - for (const [k, v] of entries.slice(0, 10)) { |
123 | | - if (typeof v === 'number') nameToIdMode++; |
124 | | - if (!isNaN(+k) && typeof v === 'string') idToNameMode++; |
125 | | - } |
126 | | - if (nameToIdMode >= idToNameMode) { |
127 | | - for (const [name, id] of entries) { |
128 | | - const num = Number(id); |
129 | | - if (Number.isFinite(num)) rev.set(num, name); |
130 | | - } |
131 | | - } else { |
132 | | - for (const [idStr, name] of entries) { |
133 | | - const num = Number(idStr); |
134 | | - if (Number.isFinite(num) && typeof name === 'string') rev.set(num, name); |
135 | | - } |
136 | | - } |
137 | | - } |
138 | | - attackIdToName = rev; |
139 | | - status(`Custom event type map loaded (${rev.size} entries). Re-rendering…`); |
140 | | - // If a dataset was already rendered, we need to re-decode attacks and re-render. |
141 | | - // Easier: trigger reload of default CSV if present, else wait for user CSV. |
142 | | - // We'll look for currently loaded arcs' underlying data isn't stored; keep a cached last CSV rows. |
143 | | - if (lastRawCsvRows) { |
144 | | - render(rebuildDataFromRawRows(lastRawCsvRows)); |
145 | | - } |
146 | | - } catch (err) { |
147 | | - console.error(err); |
148 | | - status('Failed to parse event type JSON.'); |
149 | | - } |
150 | | - }); |
151 | 150 |
|
152 | 151 | // Allow user to upload a custom ip_map JSON (expected format: { "1.2.3.4": 123, ... } OR reverse { "123": "1.2.3.4" }) |
153 | 152 | ipMapInput?.addEventListener('change', async (e) => { |
|
180 | 179 | } |
181 | 180 | ipIdToAddr = rev; |
182 | 181 | ipMapLoaded = true; |
| 182 | + console.log(`Custom IP map loaded with ${rev.size} entries`); |
| 183 | + console.log('Sample entries:', Array.from(rev.entries()).slice(0, 5)); |
183 | 184 | status(`Custom IP map loaded (${rev.size} entries). Re-rendering…`); |
184 | 185 | if (lastRawCsvRows) { |
185 | 186 | // rebuild to decode IP ids again |
|
209 | 210 | attack: attackName, |
210 | 211 | attack_group: attackGroupName, |
211 | 212 | }; |
212 | | - }).filter(d => isFinite(d.timestamp) && d.src_ip && d.dst_ip); |
| 213 | + }).filter(d => { |
| 214 | + // Filter out records with invalid data |
| 215 | + const hasValidTimestamp = isFinite(d.timestamp); |
| 216 | + const hasValidSrcIp = d.src_ip && d.src_ip !== 'N/A' && !d.src_ip.startsWith('IP_'); |
| 217 | + const hasValidDstIp = d.dst_ip && d.dst_ip !== 'N/A' && !d.dst_ip.startsWith('IP_'); |
| 218 | + return hasValidTimestamp && hasValidSrcIp && hasValidDstIp; |
| 219 | + }); |
213 | 220 | } |
214 | 221 |
|
215 | 222 | async function tryLoadDefaultCsv() { |
|
234 | 241 | attack: attackName, |
235 | 242 | attack_group: attackGroupName, |
236 | 243 | }; |
237 | | - }).filter(d => isFinite(d.timestamp) && d.src_ip && d.dst_ip); |
| 244 | + }).filter(d => { |
| 245 | + // Filter out records with invalid data |
| 246 | + const hasValidTimestamp = isFinite(d.timestamp); |
| 247 | + const hasValidSrcIp = d.src_ip && d.src_ip !== 'N/A' && !d.src_ip.startsWith('IP_'); |
| 248 | + const hasValidDstIp = d.dst_ip && d.dst_ip !== 'N/A' && !d.dst_ip.startsWith('IP_'); |
| 249 | + return hasValidTimestamp && hasValidSrcIp && hasValidDstIp; |
| 250 | + }); |
238 | 251 |
|
239 | | - if (!data.length) return; |
240 | | - status(`Loaded default: 90min_day1_attacks.csv (${data.length} rows)`); |
| 252 | + if (!data.length) { |
| 253 | + status('Default CSV loaded but no valid rows found. Check IP mappings.'); |
| 254 | + return; |
| 255 | + } |
| 256 | + |
| 257 | + // Report how many rows were filtered out |
| 258 | + const totalRows = rows.length; |
| 259 | + const filteredRows = totalRows - data.length; |
| 260 | + if (filteredRows > 0) { |
| 261 | + status(`Loaded default: 90min_day1_attacks.csv (${data.length} valid rows, ${filteredRows} filtered due to missing IP mappings)`); |
| 262 | + } else { |
| 263 | + status(`Loaded default: 90min_day1_attacks.csv (${data.length} rows)`); |
| 264 | + } |
| 265 | + |
241 | 266 | render(data); |
242 | 267 | } catch (err) { |
243 | 268 | // ignore if file isn't present; keep waiting for upload |
|
279 | 304 | // Determine timestamp handling |
280 | 305 | const tsMin = d3.min(data, d => d.timestamp); |
281 | 306 | const tsMax = d3.max(data, d => d.timestamp); |
| 307 | + // Check if timestamps are in milliseconds (very large numbers) or minutes |
| 308 | + const looksLikeMilliseconds = tsMin > 1e12; // heuristic: milliseconds since epoch |
282 | 309 | const looksAbsolute = tsMin > 1e6; // heuristic: minutes since epoch |
283 | 310 | const base = looksAbsolute ? 0 : tsMin; // for relative minutes, normalize to 0 |
| 311 | + |
| 312 | + console.log('Timestamp debug:', { |
| 313 | + tsMin, |
| 314 | + tsMax, |
| 315 | + looksLikeMilliseconds, |
| 316 | + looksAbsolute, |
| 317 | + base, |
| 318 | + sampleTimestamps: data.slice(0, 5).map(d => d.timestamp) |
| 319 | + }); |
284 | 320 |
|
285 | | - const toDate = (m) => new Date((looksAbsolute ? m : (m - base)) * 60_000); |
| 321 | + const toDate = (m) => { |
| 322 | + if (m === undefined || m === null || !isFinite(m)) { |
| 323 | + console.warn('Invalid timestamp in toDate:', m); |
| 324 | + return new Date(0); // Return epoch as fallback |
| 325 | + } |
| 326 | + |
| 327 | + let result; |
| 328 | + if (looksLikeMilliseconds) { |
| 329 | + // Timestamp is already in milliseconds |
| 330 | + result = new Date(m); |
| 331 | + } else if (looksAbsolute) { |
| 332 | + // Timestamp is in minutes since epoch |
| 333 | + result = new Date(m * 60_000); |
| 334 | + } else { |
| 335 | + // Timestamp is relative minutes |
| 336 | + result = new Date((m - base) * 60_000); |
| 337 | + } |
| 338 | + |
| 339 | + if (!isFinite(result.getTime())) { |
| 340 | + console.warn('Invalid date result in toDate:', { |
| 341 | + m, |
| 342 | + looksLikeMilliseconds, |
| 343 | + looksAbsolute, |
| 344 | + base, |
| 345 | + result |
| 346 | + }); |
| 347 | + return new Date(0); // Return epoch as fallback |
| 348 | + } |
| 349 | + return result; |
| 350 | + }; |
286 | 351 |
|
287 | 352 | // Aggregate links; then order IPs using the React component's approach: |
288 | 353 | // primary-attack grouping, groups ordered by earliest time, nodes within group by force-simulated y |
289 | 354 | const links = computeLinks(data); // aggregated per pair per minute |
290 | 355 | const nodes = computeNodesByAttackGrouping(links); |
291 | 356 | const ips = nodes.map(n => n.name); |
| 357 | + |
| 358 | + console.log('Render debug:', { |
| 359 | + dataLength: data.length, |
| 360 | + linksLength: links.length, |
| 361 | + nodesLength: nodes.length, |
| 362 | + ipsLength: ips.length, |
| 363 | + sampleIps: ips.slice(0, 5), |
| 364 | + sampleLinks: links.slice(0, 3) |
| 365 | + }); |
292 | 366 | // Determine which label dimension we use (attack vs group) for legend and coloring |
293 | 367 | const activeLabelKey = labelMode === 'attack_group' ? 'attack_group' : 'attack'; |
294 | 368 | const attacks = Array.from(new Set(links.map(l => l[activeLabelKey] || 'normal'))).sort(); |
|
300 | 374 | height = margin.top + innerHeight + margin.bottom; |
301 | 375 | svg.attr('width', width).attr('height', height); |
302 | 376 |
|
| 377 | + const xMinDate = toDate(tsMin); |
| 378 | + const xMaxDate = toDate(tsMax); |
| 379 | + |
| 380 | + console.log('X-scale debug:', { |
| 381 | + tsMin, |
| 382 | + tsMax, |
| 383 | + xMinDate, |
| 384 | + xMaxDate, |
| 385 | + xMinValid: isFinite(xMinDate.getTime()), |
| 386 | + xMaxValid: isFinite(xMaxDate.getTime()) |
| 387 | + }); |
| 388 | + |
303 | 389 | const x = d3.scaleTime() |
304 | | - .domain([toDate(tsMin), toDate(tsMax)]) |
| 390 | + .domain([xMinDate, xMaxDate]) |
305 | 391 | .range([margin.left, width - margin.right]); |
306 | 392 |
|
307 | 393 | const y = d3.scalePoint() |
308 | 394 | .domain(ips) |
309 | 395 | .range([margin.top, margin.top + innerHeight]) |
310 | 396 | .padding(0.5); |
| 397 | + |
| 398 | + console.log('Y-scale debug:', { |
| 399 | + domain: ips, |
| 400 | + domainLength: ips.length, |
| 401 | + sampleYValues: ips.slice(0, 5).map(ip => ({ ip, y: y(ip) })) |
| 402 | + }); |
311 | 403 |
|
312 | 404 | // Compute a right-side padding so the largest arc does not get clipped. |
313 | 405 | // The horizontal reach of an arc equals its radius = |y2 - y1|/2. |
|
391 | 483 |
|
392 | 484 | // Arc path generator between two points sharing same x |
393 | 485 | function verticalArcPath(xp, y1, y2) { |
| 486 | + // Validate inputs to prevent undefined values in SVG path |
| 487 | + if (xp === undefined || y1 === undefined || y2 === undefined) { |
| 488 | + console.warn('Invalid arc path parameters:', { xp, y1, y2 }); |
| 489 | + return 'M0,0 L0,0'; // Return a minimal valid path |
| 490 | + } |
394 | 491 | const yTop = Math.min(y1, y2); |
395 | 492 | const yBot = Math.max(y1, y2); |
396 | 493 | const dr = Math.max(1, (yBot - yTop) / 2); |
|
410 | 507 | .attr('stroke', d => colorForAttack((labelMode==='attack_group'? d.attack_group : d.attack) || 'normal')) |
411 | 508 | .attr('stroke-width', d => widthScale(Math.max(1, d.count))) |
412 | 509 | .attr('d', d => { |
413 | | - const xp = x(toDate(d.minute)); |
| 510 | + const dateFromMinute = toDate(d.minute); |
| 511 | + const xp = x(dateFromMinute); |
414 | 512 | const y1 = y(d.source); |
415 | 513 | const y2 = y(d.target); |
| 514 | + |
| 515 | + // Validate that x-scale returned valid values |
| 516 | + if (xp === undefined || !isFinite(xp)) { |
| 517 | + console.warn('Invalid x-coordinate for arc:', { |
| 518 | + minute: d.minute, |
| 519 | + dateFromMinute, |
| 520 | + xp, |
| 521 | + xDomain: [xMinDate, xMaxDate], |
| 522 | + xRange: [margin.left, width - margin.right] |
| 523 | + }); |
| 524 | + return 'M0,0 L0,0'; // Return minimal valid path |
| 525 | + } |
| 526 | + |
| 527 | + // Validate that y-scale returned valid values |
| 528 | + if (y1 === undefined || y2 === undefined) { |
| 529 | + console.warn('Invalid y-coordinates for arc:', { |
| 530 | + source: d.source, |
| 531 | + target: d.target, |
| 532 | + y1, |
| 533 | + y2, |
| 534 | + xp, |
| 535 | + minute: d.minute, |
| 536 | + yDomain: ips, |
| 537 | + sourceInDomain: ips.includes(d.source), |
| 538 | + targetInDomain: ips.includes(d.target) |
| 539 | + }); |
| 540 | + return 'M0,0 L0,0'; // Return minimal valid path |
| 541 | + } |
| 542 | + |
416 | 543 | return verticalArcPath(xp, y1, y2); |
417 | 544 | }) |
418 | 545 | .on('mouseover', function (event, d) { |
|
717 | 844 | if (Number.isFinite(n) && ipIdToAddr) { |
718 | 845 | const ip = ipIdToAddr.get(n); |
719 | 846 | if (ip) return ip; |
| 847 | + // If IP ID not found in map, log it and return a placeholder |
| 848 | + console.warn(`IP ID ${n} not found in mapping. Available IDs: ${ipIdToAddr ? ipIdToAddr.size : 0} entries`); |
| 849 | + return `IP_${n}`; |
720 | 850 | } |
721 | 851 | return v; // fallback to original string |
722 | 852 | } |
|
0 commit comments