Security Command Centre
A security operations console that pulls sign-in, incident and email-threat telemetry from Microsoft 365, runs detections against it, and tracks what it finds through a real incident lifecycle.
Evidence for
- AI integrations and automation
- Internal tools and operational software
- Custom business systems
Built on
- TypeScript
- Node
- React
- Microsoft Graph
- Entra ID
- Vitest
Measured
- Detection types
- 7
server/db/store.ts- Incident states
- 14
IncidentStatus, server/db/store.ts- Response actions
- 6
server/services/responseActions.ts- Language models
- 0
no LLM SDK in dependencies or devDependencies- Tests
- 105
vitest run, 11 files
The build is branded throughout for the organisation it was made for — their name is in the shell, the page title and the theme. Those screens are not ours to publish, and stripping the branding to publish them anyway would be presenting someone else's system as ours.
The problem it was built for#
Security telemetry arrives as a firehose of events with no memory. Something is flagged, somebody looks at it, and there is no record of who decided what or why. So the same alert is investigated from scratch the next time it appears, and the answer to "why was this dismissed in March?" is that nobody knows.
The detections themselves are the well-trodden part: impossible travel, password spray, dormant accounts, phishing and malware in mail, risky URL clicks, and incidents forwarded on from Microsoft Defender. Each is mapped to its MITRE ATT&CK techniques and tactics, so a finding arrives already in the vocabulary the industry argues in.
The lifecycle is the product#
A detection is worth very little. A detection that can be assigned, triaged, investigated, contained and closed — with a classification that is mandatory and an audit trail that survives the analyst leaving — is worth a great deal.
Fourteen states, and the ones worth pointing at are the awkward ones:
| State | Why it exists |
|---|---|
| Waiting for user | Blocked on a human who has not replied. Not the analyst's fault, and not progress either. |
| Waiting for vendor | Same, for a third party. Distinguished because the escalation path is different. |
| Accepted risk | Reviewed, understood, and deliberately not actioned. A decision, recorded as one. |
| Suppressed | Known benign or not applicable. Stops the same finding consuming attention forever. |
Closing requires a classification — true positive, false positive, benign, informational or duplicate — and it cannot be skipped. That single constraint is what turns a queue into a record you can answer questions from months later.
Actions declare what they will do before they do it#
Six response actions can be taken against an account. Each one is a declaration rather than a function: it states the exact Microsoft Graph permission it needs, whether it is reversible and what its inverse is, the precise call it will make, whether it requires an Entra ID P2 licence, and — this one caught us — which identifier Graph will actually accept, because some endpoints take a user principal name and some insist on an object ID and fail confusingly if given the wrong one.
Several also carry a propagation caveat, stated in the interface at the moment of use rather than buried in documentation. Signing someone out everywhere does not take effect instantly; Microsoft can take minutes to invalidate tokens already issued. An analyst who believes the account is contained when it is not is worse off than one who was told to wait.
Two guards refuse actions outright:
- You cannot action your own account. Ask a colleague, so you do not lock yourself out mid-incident.
- If the directory-role check could not run, the action is blocked. Not allowed with a warning — blocked. An empty list of roles means "verified, holds none". A missing list means "we could not check", and the two are deliberately different values, because treating the second as the first is how you disable a global administrator by accident.
Demo mode cannot reach a live tenant#
The whole system runs in a demo mode against synthetic data, and in that mode
response actions are pinned to simulate — regardless of what the environment
variable says. Setting the mode to live while demo mode is on does nothing.
There is a test whose only job is to assert that, and it is the most important test in the repository. A demonstration that can disable a real person's account is not a demonstration.
What we learned#
This is the one we would point at when asked what we think about AI.
It is called an AI security analyst. There is no language model anywhere in it.
Every recommendation it makes has to be explainable to somebody who was not there, and identical every time it runs. Those two requirements are not compatible with a generative model, and the moment we wrote them down the architecture decided itself: deterministic rules, mapped to a public framework, with the reasoning visible.
We could have shipped something that sounded more impressive. It would have been worse software. Choosing the boring answer over the fashionable one is the judgement we would want a client to be buying, and it is the reason this project is on this page rather than a project with a chatbot in it.