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();