For reasons I had about 28k incidents I needed to close in Azure Sentinel, and the interface will only allow me to bulk close 50 at a time. What to do?
Closing 28k incidents would mean 7 clicks * 28 000 = 196 000 clicks. About 196 000 more than I wanted to do.
I could automate this using a tool like AutoIt, but I didn’t want to install a tool just to do this. I could explore the Azure API and write a script. Or, I could let my browser do the work for me - no other tool needed.
JavaScript Console to the rescue!
It starts with faking a click
This will be easy
-me before I started
I thought I could simply fake the clicks by using document.getElementById()
and dispatching a click-event. Turns out I was wrong. This worked for the checkbox to select all incidents, and for the Actions-button. But not for selecting the new status and the reason.
It took a lot of digging and experimenting before I learned that I can manipulate the Knockout.js (which is what’s used in Azure) ViewModel vi ko.dataFor()
. I had it right the first time I tried it, but the object had changed between the time I put it in a variable and the time I figured out which property to set. So it took a lot of not getting anywhere before I realized I had to look at class names instead of ids. Apparently Knockout.js changes the id a lot!
Timeout-based development
Since this is a hack, I used a lot of dirty tricks to get the correct element, and eventually even added some error checking. But eventually it all came together to do what I wanted - select all incidents, open the Actions pane, set status to Closed and reason to Undetermined, and then click Apply. I’m lazy, so I opted for the highly accurate setTimeout()
way of making sure it was finished before I started on the next batch.
It crashed a couple times before I adjusted the timing and added some error checking. After I finished tweaking it, I was successfully able to let this run for about 1-2 hours, and after that it was done! I only had to manually set the status and reason and click Apply once, after that it continued where it got stuck.
So this hack did exactly what I needed it to do, maybe it can help someone else as well?
The code
⚠️ Blindly running code from the internet can be dangerous ⚠️
Please read through and understand what this code does before trying to use it.
You also need to set the appropriate incident filter first, and set a correct maxRunCount
-number (did I mention I was lazy? I didn’t know how to nor want to spend time on figuring out how to detect when it was done).
/* USE AT YOUR OWN RISK - I TAKE NO RESPONSIBILITY FOR ANYTHING THAT HAPPENS WHEN YOU RUN THIS */
let hackRuntimeCounter = 0;
const maxRunCount = 100;
const doCloseIncidentsHack = () => {
console.log(`Doing run ${hackRuntimeCounter} of ${maxRunCount}`);
// Get checkbox
const checkbox = [...document.querySelectorAll(".azc-checkBox")]
.filter(
(x) =>
x.attributes.getNamedItem("aria-label").value === "Select all items"
)
.at(0).children[0];
checkbox.dispatchEvent(
new MouseEvent("click", { bubbles: true, cancelable: true })
);
// We have to wait for the actions button to be ready
setTimeout(() => {
let actionsButton = [
...document.querySelectorAll(".fxs-commandBar-item-text"),
]
.filter((x) => x.innerText == "Actions")
.at(0);
// Wait a bit and retry if the button is still disabled
if (actionsButton.parentElement.classList.contains("azc-text-disabled")) {
setTimeout(() => {
doCloseIncidentsHack();
}, 6000);
return;
}
actionsButton.dispatchEvent(
new MouseEvent("click", { bubbles: true, cancelable: true })
);
// We have to wait for the panel to load
setTimeout(() => {
let elementContainer = [
...document.getElementsByClassName("fxs-blade-content-container"),
]
.filter(
(x) =>
x.parentElement.children[0].innerText.split("\n").at(0) ===
"Actions"
)
.at(0);
if (elementContainer === undefined) {
setTimeout(() => {
doCloseIncidentsHack();
}, 6000);
return;
}
// Get ViewModel and update data
let viewModel = ko.dataFor(
elementContainer.getElementsByClassName(
"azc-formElementSubLabelContainer"
)[3]
);
viewModel.caseStatusDropDownControl.value(4); // 4 = Closed
viewModel.isCaseClosed(true);
viewModel.caseCloseReasonControl.closeReasonDropDown.value({
classification: 6, // 6 = Undetermined
classificationReason: null,
});
// Let's do this!
viewModel.buttons()[0].onClick();
// Continue as long as we haven't reached our configured number of executions
if (hackRuntimeCounter <= maxRunCount) {
setTimeout(() => {
doCloseIncidentsHack();
}, 6000);
}
hackRuntimeCounter++;
}, 1800);
}, 800);
};
doCloseIncidentsHack();