Skip to content

Commit 8d1b960

Browse files
committed
Added a new module to query Microsoft Azure CIDRs.
1 parent 403a404 commit 8d1b960

File tree

8 files changed

+969
-5
lines changed

8 files changed

+969
-5
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ Please see https://github.com/elelabdev/net-commander/releases for the latest re
33
All notable changes to the "net-commander" extension will be documented in this file.
44

55
<br><br>
6-
## 2025-06-20 - [0.0.3]
6+
## 2025-09-16 - [0.0.9]
77
### Added
8+
- Added a new module to query Microsoft Azure CIDRs. You can now query right from Visual Studio Code all Microsoft Azure subscriptions and search for specific CIDRs in use.
9+
10+
## 2025-06-20 - [0.0.3]
11+
### Minor fix
812
- Minor fixes for Root Cause Analysis for Microsoft Azure
913

1014
## 2025-05-16 - [0.0.2]
11-
### Added
15+
### Minor fix
1216
- Minor fixes for Windows Tracert Command
1317

1418
## 2025-05-16 - [0.0.1]

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ Aimed to be a powerful all-in-one toolkit for Network Engineers, DevOps Engineer
5959

6060
<br>
6161

62+
## Microsoft Azure CIDRs Analyzer
63+
![Microsoft Azure CIDRs Analyzer](https://raw.githubusercontent.com/elelabdev/net-commander/refs/heads/main/media/img/readme/microsoft-azure-cidr-analyzer.png)
64+
You can now query right from Visual Studio Code all Microsoft Azure subscriptions and search for specific CIDRs in use, this will help you understand if the CIDR is in use to avoid overlapping. Net Commander allow also to search all CIDRs in use at once, great for reporting and you can easily export the results in CSV right in your project folder.
65+
66+
<br>
67+
6268
## Root Cause Analysis
6369
![Root Cause Analysis](https://raw.githubusercontent.com/elelabdev/net-commander/refs/heads/main/media/img/readme/rootcause-analysis.gif)
6470
Root-cause analysis must be **data-driven** and free from assumptions or feelings.
50.1 KB
Loading
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
/***************************************************************************
2+
* Extension: Net Commander *
3+
* Author: elelabdev *
4+
* Description: Net Commander is the extension for Visual Studio Code *
5+
* dedicated to Network Engineers, DevOps Engineers and *
6+
* Solution Architects streamlining everyday workflows and *
7+
* accelerating data-driven root-cause analysis. *
8+
* *
9+
* Github: https://github.com/elelabdev/net-commander *
10+
* *
11+
* Icon Author: elelab *
12+
* *
13+
* Copyright (C) 2025 elelab *
14+
* https://www.elelab.dev *
15+
* *
16+
* Licensed under the MIT License. See LICENSE file in the project *
17+
* root for details. *
18+
**************************************************************************/
19+
20+
// media/module-azurecidranalyzer/main.js
21+
22+
// Suppress noisy ResizeObserver loop errors in the webview console
23+
window.addEventListener('error', (event) => {
24+
if (event.message?.includes('ResizeObserver loop completed with undelivered notifications')) {
25+
event.preventDefault();
26+
}
27+
});
28+
29+
document.addEventListener('DOMContentLoaded', () => {
30+
const vscode = acquireVsCodeApi();
31+
const cidrInput = document.getElementById('cidrInput');
32+
const searchBtn = document.getElementById('searchBtn');
33+
const exportBtn = document.getElementById('exportBtn');
34+
const subscriptionOptionsDiv = document.getElementById('subscriptionOptions');
35+
const subscriptionsProgressDiv = document.getElementById('subscriptions');
36+
const resultsDiv = document.getElementById('results');
37+
const statusEl = document.getElementById('status');
38+
39+
if (!cidrInput || !searchBtn || !exportBtn || !subscriptionOptionsDiv || !subscriptionsProgressDiv || !resultsDiv || !statusEl) {
40+
console.error('Azure CIDR Analyzer: missing DOM elements');
41+
return;
42+
}
43+
44+
const subscriptionState = {
45+
options: [],
46+
selected: new Set()
47+
};
48+
49+
const setStatus = (message, kind = 'info') => {
50+
statusEl.textContent = message || '';
51+
statusEl.className = kind ? kind : '';
52+
};
53+
54+
const setLoading = (value) => {
55+
if (value) {
56+
searchBtn.disabled = true;
57+
exportBtn.disabled = true;
58+
setStatus('Searching Azure Resource Graph…', 'loading');
59+
} else if (!statusEl.textContent) {
60+
searchBtn.disabled = false;
61+
setStatus('', '');
62+
} else {
63+
searchBtn.disabled = false;
64+
}
65+
};
66+
67+
const renderEmpty = (message) => {
68+
resultsDiv.innerHTML = '';
69+
const p = document.createElement('p');
70+
p.className = 'empty-state';
71+
p.textContent = message;
72+
resultsDiv.appendChild(p);
73+
};
74+
75+
const resetSubscriptionsView = () => {
76+
subscriptionsProgressDiv.innerHTML = '';
77+
};
78+
79+
const ensureAllCheckbox = () => {
80+
const allCheckbox = subscriptionOptionsDiv.querySelector('input[data-id="__all__"]');
81+
if (!allCheckbox) return;
82+
allCheckbox.checked = subscriptionState.selected.size === 0;
83+
};
84+
85+
const renderSubscriptionOptions = (options) => {
86+
subscriptionState.options = options;
87+
subscriptionState.selected.clear();
88+
subscriptionOptionsDiv.innerHTML = '';
89+
90+
const buildCheckbox = (labelText, value, checked = false, disabled = false) => {
91+
const wrapper = document.createElement('label');
92+
wrapper.className = 'subscription-option';
93+
94+
const checkbox = document.createElement('input');
95+
checkbox.type = 'checkbox';
96+
checkbox.dataset.id = value;
97+
checkbox.checked = checked;
98+
checkbox.disabled = disabled;
99+
100+
const label = document.createElement('span');
101+
label.textContent = labelText;
102+
103+
wrapper.append(checkbox, label);
104+
return { wrapper, checkbox };
105+
};
106+
107+
const allEntry = buildCheckbox('All subscriptions', '__all__', true, options.length === 0);
108+
allEntry.checkbox.addEventListener('change', () => {
109+
if (allEntry.checkbox.checked) {
110+
subscriptionState.selected.clear();
111+
subscriptionOptionsDiv.querySelectorAll('input[type="checkbox"]').forEach(cb => {
112+
if (cb.dataset.id && cb.dataset.id !== '__all__') {
113+
cb.checked = false;
114+
}
115+
});
116+
} else if (subscriptionState.selected.size === 0) {
117+
allEntry.checkbox.checked = true;
118+
}
119+
});
120+
subscriptionOptionsDiv.appendChild(allEntry.wrapper);
121+
122+
options.forEach(option => {
123+
const label = option.name ? `${option.name} (${option.id})` : option.id;
124+
const { wrapper, checkbox } = buildCheckbox(label, option.id, false, false);
125+
checkbox.addEventListener('change', () => {
126+
if (checkbox.checked) {
127+
subscriptionState.selected.add(option.id);
128+
allEntry.checkbox.checked = false;
129+
} else {
130+
subscriptionState.selected.delete(option.id);
131+
if (subscriptionState.selected.size === 0) {
132+
allEntry.checkbox.checked = true;
133+
}
134+
}
135+
});
136+
subscriptionOptionsDiv.appendChild(wrapper);
137+
});
138+
139+
ensureAllCheckbox();
140+
};
141+
142+
const createSubscriptionProgressItem = (subscription) => {
143+
const wrapper = document.createElement('div');
144+
wrapper.className = 'subscription-item';
145+
wrapper.dataset.id = subscription.id;
146+
147+
const icon = document.createElement('span');
148+
icon.className = 'status-icon loading';
149+
150+
const label = document.createElement('span');
151+
label.className = 'subscription-label';
152+
label.textContent = subscription.name
153+
? `${subscription.name} (${subscription.id})`
154+
: subscription.id;
155+
156+
const note = document.createElement('span');
157+
note.className = 'subscription-note';
158+
note.textContent = 'Pending…';
159+
160+
wrapper.append(icon, label, note);
161+
subscriptionsProgressDiv.appendChild(wrapper);
162+
};
163+
164+
const updateSubscriptionStatus = (subscriptionId, status, detail) => {
165+
const item = subscriptionsProgressDiv.querySelector(`[data-id="${subscriptionId}"]`);
166+
if (!item) {
167+
return;
168+
}
169+
const icon = item.querySelector('.status-icon');
170+
const note = item.querySelector('.subscription-note');
171+
icon.classList.remove('loading', 'success', 'error');
172+
173+
switch (status) {
174+
case 'running':
175+
icon.classList.add('loading');
176+
note.textContent = 'Searching…';
177+
break;
178+
case 'done':
179+
icon.classList.add('success');
180+
note.textContent = `${detail?.count ?? 0} match${(detail?.count ?? 0) === 1 ? '' : 'es'}`;
181+
break;
182+
case 'error':
183+
icon.classList.add('error');
184+
note.textContent = detail?.message ? `Error: ${detail.message}` : 'Error';
185+
break;
186+
default:
187+
note.textContent = '';
188+
break;
189+
}
190+
};
191+
192+
const renderResults = (results, cidrs, columns) => {
193+
const joinedCidrs = Array.isArray(cidrs) ? cidrs.join(', ') : '';
194+
195+
if (!Array.isArray(results) || results.length === 0) {
196+
renderEmpty(`No results for ${joinedCidrs || 'your query'}.`);
197+
exportBtn.disabled = true;
198+
return;
199+
}
200+
201+
resultsDiv.innerHTML = '';
202+
203+
const table = document.createElement('vscode-table');
204+
table.zebra = true;
205+
table['bordered-rows'] = true;
206+
207+
const header = document.createElement('vscode-table-header');
208+
header.slot = 'header';
209+
const safeColumns = Array.isArray(columns) && columns.length ? columns : Object.keys(results[0] || {});
210+
safeColumns.forEach((col) => {
211+
const cell = document.createElement('vscode-table-header-cell');
212+
cell.textContent = col;
213+
header.appendChild(cell);
214+
});
215+
table.appendChild(header);
216+
217+
const body = document.createElement('vscode-table-body');
218+
body.slot = 'body';
219+
220+
results.forEach((row) => {
221+
const tr = document.createElement('vscode-table-row');
222+
safeColumns.forEach((col) => {
223+
const td = document.createElement('vscode-table-cell');
224+
const value = row ? row[col] : undefined;
225+
if (value === null || value === undefined) {
226+
td.textContent = '';
227+
} else if (typeof value === 'object') {
228+
td.textContent = JSON.stringify(value);
229+
} else {
230+
td.textContent = String(value);
231+
}
232+
tr.appendChild(td);
233+
});
234+
body.appendChild(tr);
235+
});
236+
237+
table.appendChild(body);
238+
resultsDiv.appendChild(table);
239+
exportBtn.disabled = false;
240+
};
241+
242+
searchBtn.addEventListener('click', () => {
243+
const cidr = cidrInput.value.trim();
244+
if (!cidr) {
245+
setStatus('Enter a CIDR, e.g. 10.10.0.0/16', 'error');
246+
cidrInput.focus();
247+
return;
248+
}
249+
resetSubscriptionsView();
250+
resultsDiv.innerHTML = '';
251+
exportBtn.disabled = true;
252+
253+
const selectedIds = Array.from(subscriptionState.selected);
254+
ensureAllCheckbox();
255+
256+
vscode.postMessage({
257+
command: 'lookupCidr',
258+
cidr,
259+
subscriptions: selectedIds.length ? selectedIds : undefined
260+
});
261+
});
262+
263+
exportBtn.addEventListener('click', () => {
264+
vscode.postMessage({ command: 'exportCsv' });
265+
});
266+
267+
// Request subscription options on load
268+
vscode.postMessage({ command: 'requestSubscriptions' });
269+
270+
window.addEventListener('message', (event) => {
271+
const data = event.data;
272+
if (!data) {
273+
return;
274+
}
275+
276+
switch (data.command) {
277+
case 'setLoading':
278+
setLoading(!!data.value);
279+
break;
280+
case 'subscriptionOptions':
281+
renderSubscriptionOptions(data.subscriptions || []);
282+
break;
283+
case 'initSubscriptions':
284+
resetSubscriptionsView();
285+
if (Array.isArray(data.subscriptions)) {
286+
data.subscriptions.forEach(createSubscriptionProgressItem);
287+
}
288+
break;
289+
case 'subscriptionStatus':
290+
updateSubscriptionStatus(data.subscriptionId, data.status, { count: data.count, message: data.message });
291+
break;
292+
case 'displayResults':
293+
renderResults(data.results, data.cidrs, data.columns);
294+
setStatus(
295+
`Found ${data.results.length} entr${data.results.length === 1 ? 'y' : 'ies'} for ${Array.isArray(data.cidrs) ? data.cidrs.join(', ') : 'your query'}.`,
296+
data.results.length ? 'info' : 'warning'
297+
);
298+
break;
299+
case 'showError':
300+
setStatus(data.message || 'Unexpected error.', 'error');
301+
exportBtn.disabled = true;
302+
break;
303+
case 'showInfo':
304+
setStatus(data.message || '', 'info');
305+
if (data.message?.includes('No matches')) {
306+
renderEmpty(data.message);
307+
}
308+
break;
309+
default:
310+
break;
311+
}
312+
});
313+
});

0 commit comments

Comments
 (0)