Cloud
banshee/cloud-collection/cloud/azure/https/
service-health
The active Azure service issues, planned maintenances and advisories that touch an Azure subscription, by Azure Resource Health.
by banshee1 installation2 versions
// The Azure service health events that touch a subscription, by Azure Resource Health, logged in as a service
// principal: how many Azure service issues are active, how many planned maintenances and advisories, and the
// titles of the issues.
// Reads POST https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token, then GET
// /subscriptions/<id>/providers/Microsoft.ResourceHealth/events on management.azure.com, page after page.
need('protocol:http');
$client = need('protocol:http:user');
$secret = need('protocol:http:password');
$tenant = variable('azure:tenant', 'text', array(
'label' => 'Tenant',
'help' => 'The directory (tenant) ID of the service principal, or one of the tenant\'s domains.',
));
$subscription = variable('azure:subscription', 'text', array(
'label' => 'Subscription',
'help' => 'The ID of the subscription whose service health events are read.',
));
output('issues', 'number', array('label' => 'Active service issues'));
output('maintenances', 'number', array('label' => 'Active planned maintenances'));
output('advisories', 'number', array('label' => 'Active advisories'));
output('issue-titles', 'text', array('label' => 'Titles of the active service issues'));
watch.above('issue', 'Azure service issue', 'issues', array('warning' => 0));
watch.above('maintenance', 'Azure planned maintenance', 'maintenances', array('warning' => 0, 'enabled' => false));
watch.above('advisory', 'Azure advisory', 'advisories', array('warning' => 0, 'enabled' => false));
// Log in as the service principal, by the client credentials grant. The secret goes in the form, which makes
// the answer secret, and its access token with it. The token is kept in the cache, encrypted on the poller,
// thirty minutes, under the hour or so Microsoft gives it.
function login($tenant, $client, $secret, $scope) {
$body = url.query(array('grant_type' => 'client_credentials', 'client_id' => $client,
'client_secret' => $secret, 'scope' => $scope));
$answer = http.post('https://login.microsoftonline.com/' . url.encode($tenant) . '/oauth2/v2.0/token', $body,
headers=array('Content-Type' => 'application/x-www-form-urlencoded'));
if ($answer['status'] == 400 || $answer['status'] == 401) {
fail('misconfigured', 'Microsoft Entra ID refuses the service principal, code ' . $answer['status']);
}
if ($answer['status'] != 200) {
fail('unreachable', 'Microsoft Entra ID answers the login with code ' . $answer['status']);
}
$session = json.read($answer['body']);
cache.set('token', $session['access_token'], 1800);
return $session['access_token'];
}
// One GET on Azure Resource Manager with the token kept in the cache; a token Azure no longer takes answers
// 401, and the collector logs in again, once.
function arm_get($url, $what, $tenant, $client, $secret) {
if (cache.has('token')) {
$token = cache.get('token');
} else {
$token = login($tenant, $client, $secret, 'https://management.azure.com/.default');
}
$answer = http.get($url, headers=array('Authorization' => 'Bearer ' . $token));
if ($answer['status'] == 401) {
$token = login($tenant, $client, $secret, 'https://management.azure.com/.default');
$answer = http.get($url, headers=array('Authorization' => 'Bearer ' . $token));
}
if ($answer['status'] == 403) {
fail('misconfigured', 'The service principal may not read ' . $what . ', code 403');
}
if ($answer['status'] == 400 || $answer['status'] == 404) {
fail('misconfigured', 'Azure refuses to read ' . $what . ', code ' . $answer['status'],
'Check the IDs and names set on the probe.');
}
if ($answer['status'] != 200) {
fail('unreachable', 'Azure answers the reading of ' . $what . ' with code ' . $answer['status']);
}
return json.read($answer['body']);
}
// The items of a list, page after page: Azure gives the address of the next page under nextLink, until the
// last. A next page is only followed on Azure Resource Manager itself, where the token may go.
function arm_list($url, $what, $tenant, $client, $secret) {
$items = array();
while ($url != '') {
if (!text.like($url, 'https://management.azure.com/*')) {
fail('unreachable', 'Azure gives the next page of ' . $what . ' outside management.azure.com');
}
$page = arm_get($url, $what, $tenant, $client, $secret);
foreach (list.get($page, 'value', array()) as $item) {
$items = list.add($items, $item);
}
$url = list.get($page, 'nextLink', '');
}
return $items;
}
function is_leap($year) {
return $year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0);
}
// The date written 10/27/2026, month/day/year, of a whole number of seconds since 1 January 1970 UTC: the form
// of queryStartTime.
function utc_date($epoch) {
$days = ($epoch - $epoch % 86400) / 86400;
$year = 1970;
while (true) {
$length = 365;
if (is_leap($year)) {
$length = 366;
}
if ($days < $length) {
break;
}
$days = $days - $length;
$year = $year + 1;
}
$months = array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
if (is_leap($year)) {
$months[1] = 29;
}
$month = 1;
foreach ($months as $length) {
if ($days < $length) {
break;
}
$days = $days - $length;
$month = $month + 1;
}
return $month . '/' . ($days + 1) . '/' . $year;
}
// Azure lists, by default, the events updated in the last three days only: an issue still active but not
// updated since would drop out of the list. The events updated in the last thirty days are asked for.
$since = utc_date(number.round(time.now()) - 30 * 86400);
$url = 'https://management.azure.com/subscriptions/' . url.encode($subscription)
. '/providers/Microsoft.ResourceHealth/events?' . url.query(array('api-version' => '2025-05-01', 'queryStartTime' => $since));
// {"value": [{"name": "BC_1-FXZ", "properties": {"eventType": "ServiceIssue", "status": "Active", "title":
// "ACTIVE: Virtual machines in West US", "impact": [{"impactedService": "Virtual Machines", …}], …}}],
// "nextLink": "…"}. eventType is ServiceIssue, PlannedMaintenance, HealthAdvisory, SecurityAdvisory, RCA,
// EmergingIssues or Billing; status Active or Resolved.
$events = arm_list($url, 'the service health events', $tenant, $client, $secret);
$issues = 0;
$maintenances = 0;
$advisories = 0;
$titles = array();
foreach ($events as $event) {
$properties = list.get($event, 'properties', array());
if (list.get($properties, 'status', '') != 'Active') {
continue;
}
$type = list.get($properties, 'eventType', '');
if ($type == 'ServiceIssue') {
$issues = $issues + 1;
$titles = list.add($titles, list.get($properties, 'title', $event['name']));
} elseif ($type == 'PlannedMaintenance') {
$maintenances = $maintenances + 1;
} elseif ($type == 'HealthAdvisory' || $type == 'SecurityAdvisory') {
$advisories = $advisories + 1;
}
}
value('issues', $issues);
value('maintenances', $maintenances);
value('advisories', $advisories);
value('issue-titles', text.join($titles, ' | '));