O Sertão será Cloud

Automating Compliance Policy Assignments with Azure Functions and TypeScript

Introduction

In today’s business environment, ensuring compliance with security policies is crucial. Automating this process can significantly simplify compliance management. In this article, we will explore how to implement an Azure function to create and assign compliance policies automatically using TypeScript and the Microsoft Graph API. Based on Marcelo’s content from Cloud na Quebrada, it can be said that managing compliance policies has never been easier with automation and Azure Functions.

Environment Setup

Before we begin, you need to set up some environment variables in the Azure Portal:

Creating a Service Principal
Adding required permissions
  • CLIENT_ID: Azure AD client ID for authentication.
  • CLIENT_SECRET: Azure AD client secret.
  • TENANT_ID: Azure AD tenant ID.

Implementation of Azure Function

import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { ServicePrincipal } from "../src/service/ServicePrincipal";
import { CompliancePolicies } from "../src/service/CompliancePolicies";

/**
* HTTP trigger function for processing compliance policy assignments.
*
* @param {Context} context - The context object for the Azure Function.
* @param {HttpRequest} req - The HTTP request object.
* @returns {Promise<void>} - A promise that resolves when the function completes.
*/
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
try {
context.log('HTTP trigger function processed a request.');

// Validate request method
if (req.method !== "POST") {
context.res = {
status: 405,
body: "Method Not Allowed",
};
return;
}

// Extract environment variables
const clientId: string = String(process.env.CLIENT_ID);
const clientSecret: string = String(process.env.CLIENT_SECRET);
const tenantId: string = String(process.env.TENANT_ID);

// Initialize ServicePrincipal with Azure AD credentials
context.log('Initializing ServicePrincipal with tenantId:', tenantId);
const servicePrincipal = new ServicePrincipal(tenantId, clientId, clientSecret);

// Request access token from Azure AD
context.log('Requesting access token');
const accessToken: string = await servicePrincipal.getAccessToken();
context.log('Access token received');

// Initialize CompliancePolicies instance with request parameters
const compliancePolicies = new CompliancePolicies(req.body.name, req.body.description);
context.log('Initialized CompliancePolicies with name:', req.body.name, 'and description:', req.body.description);

// Check if complianceid is provided; if not, create a new compliance policy
let complianceid: string;
if (!req.body.complianceid) {
context.log('No complianceid provided, creating a new compliance policy');
const compliancePolicy = await compliancePolicies.postCompliancePolicy(accessToken);
complianceid = compliancePolicy.body.id;
context.log('Created compliance policy with id:', complianceid);
} else {
complianceid = req.body.complianceid;
context.log('Using provided complianceid:', complianceid);
}

// Assign the policy to the specified group
context.log('Assigning policy with complianceid:', complianceid, 'to groupId:', req.body.groupId);
const response = await compliancePolicies.assignPolicy(
complianceid,
req.body.groupId,
accessToken
);

// Prepare and send the HTTP response
context.res = {
status: 201,
body: response.body
};
context.log('Response sent');
} catch (error) {
context.log.error('Error processing request:', error.message);
context.res = {
status: 500,
body: { message: 'Internal Server Error' }
};
}
};

export default httpTrigger;

Technical Details

ServicePrincipal Class

Responsible for Azure AD authentication using ClientSecretCredential from the @azure/identity package.

import { ClientSecretCredential } from "@azure/identity";

/**
* ServicePrincipal class to handle Azure AD authentication.
*/
export class ServicePrincipal {
/** The resource URL for Azure management. */
private readonly resource: string = "https://management.azure.com/.default";

/**
* Creates an instance of ServicePrincipal.
*
* @param {string} tenantId - The Azure AD tenant ID.
* @param {string} clientId - The client ID of the Azure AD app.
* @param {string} clientSecret - The client secret of the Azure AD app.
*/
constructor(
private readonly tenantId: string,
private readonly clientId: string,
private readonly clientSecret: string
) {}

/**
* Retrieves an access token from Azure AD.
*
* @returns {Promise<string>} - A promise that resolves to the access token.
* @throws {Error} - Throws an error if the access token cannot be acquired.
*/
public async getAccessToken(): Promise<string> {
try {
const credential = new ClientSecretCredential(this.tenantId, this.clientId, this.clientSecret);
const tokenResponse = await credential.getToken(this.resource);

if (!tokenResponse) {
throw new Error("Failed to acquire access token");
}

return tokenResponse.token;
} catch (error) {
console.error("Error acquiring access token:", error);
throw new Error("Failed to acquire access token");
}
}
}

CompliancePolicy Interface

Where the data type to be sent to the Graph API is declared:

/**
* Represents a compliance policy.
*/
export interface CompliancePolicy {
/** The OData type of the policy. */
"@odata.type": string;
/** Indicates if an active firewall is required. */
activeFirewallRequired: boolean;
/** Indicates if anti-spyware is required. */
antiSpywareRequired: boolean;
/** Indicates if antivirus is required. */
antivirusRequired: boolean;
/** Indicates if BitLocker is enabled. */
bitLockerEnabled: boolean;
/** Indicates if code integrity is enabled. */
codeIntegrityEnabled: boolean;
/** Indicates if Windows Defender is enabled. */
defenderEnabled: boolean;
/** The description of the compliance policy. */
description: string;
/** Indicates if device threat protection is enabled. */
deviceThreatProtectionEnabled: boolean;
/** The required security level for device threat protection. */
deviceThreatProtectionRequiredSecurityLevel: string;
/** The display name of the compliance policy. */
displayName: string;
/** The required password type. */
passwordRequiredType: string;
/** The role scope tag IDs. */
roleScopeTagIds: string[];
/** Indicates if real-time protection is enabled. */
rtpEnabled: boolean;
/** The scheduled actions for the rule. */
scheduledActionsForRule: ScheduledActionForRule[];
/** Indicates if secure boot is enabled. */
secureBootEnabled: boolean;
/** Indicates if the signature is out of date. */
signatureOutOfDate: boolean;
/** Indicates if TPM is required. */
tpmRequired: boolean;
}

/**
* Represents a scheduled action for a rule.
*/
interface ScheduledActionForRule {
/** The name of the rule. */
ruleName: string;
/** The scheduled action configurations for the rule. */
scheduledActionConfigurations: ScheduledActionConfiguration[];
}

/**
* Represents a scheduled action configuration.
*/
interface ScheduledActionConfiguration {
/** The type of action. */
actionType: string;
/** The grace period in hours for the action. */
gracePeriodHours: number;
/** The list of email addresses to be CC'd in the notification message. */
notificationMessageCCList: string[];
/** The ID of the notification template. */
notificationTemplateId: string;
}

CompliancePolicies Class

Encapsulates logic for creating and assigning compliance policies using the Microsoft Graph API. Includes methods for posting new policies and assigning them to groups.

import axios, { AxiosRequestConfig } from "axios";
import { CompliancePolicy } from "../interfaces/CompliancePolicy";

/**
* Interface extending CompliancePolicy to include additional properties.
*/
interface OutputCompliancePolicy extends CompliancePolicy {
id: string;
createdDateTime: Date;
lastModifiedDateTime: Date;
}

/**
* CompliancePolicies class to handle creating and assigning compliance policies.
*/
export class CompliancePolicies {
/** The base URL for Microsoft Graph API. */
private graphBaseUrl: string = "https://graph.microsoft.com/beta";

/**
* Creates an instance of CompliancePolicies.
*
* @param {string} name - The display name of the compliance policy.
* @param {string} description - The description of the compliance policy.
*/
constructor(private readonly name: string, private readonly description: string) {}

/**
* Creates a new compliance policy in Microsoft Graph.
*
* @param {string} accessToken - The access token for authentication.
* @returns {Promise<{status: number, body: OutputCompliancePolicy}>} - A promise that resolves to the created compliance policy.
* @throws {Error} - Throws an error if the HTTP request fails.
*/
async postCompliancePolicy(accessToken: string) {
try {
const url = `${this.graphBaseUrl}/deviceManagement/deviceCompliancePolicies`;
const policy: CompliancePolicy = {
"@odata.type": "#microsoft.graph.windows10CompliancePolicy",
activeFirewallRequired: true,
antiSpywareRequired: true,
antivirusRequired: true,
bitLockerEnabled: true,
codeIntegrityEnabled: true,
defenderEnabled: true,
description: this.description,
deviceThreatProtectionEnabled: false,
deviceThreatProtectionRequiredSecurityLevel: "unavailable",
displayName: this.name,
passwordRequiredType: "deviceDefault",
roleScopeTagIds: ["0"],
rtpEnabled: true,
scheduledActionsForRule: [
{
ruleName: "PasswordRequired",
scheduledActionConfigurations: [
{
actionType: "block",
gracePeriodHours: 12,
notificationMessageCCList: [],
notificationTemplateId: "",
},
{
actionType: "retire",
gracePeriodHours: 4320,
notificationMessageCCList: [],
notificationTemplateId: "",
},
],
},
],
secureBootEnabled: true,
signatureOutOfDate: true,
tpmRequired: true,
};

const config: AxiosRequestConfig = {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
};

const response = await axios.post<OutputCompliancePolicy>(url, policy, config);

return {
status: 201,
body: response.data,
};
} catch (error) {
console.error("Error creating compliance policy:", error);
throw new Error("Failed to create compliance policy");
}
}

/**
* Assigns a compliance policy to a group in Microsoft Graph.
*
* @param {string} policyId - The ID of the compliance policy.
* @param {string} groupId - The ID of the group to assign the policy to.
* @param {string} accessToken - The access token for authentication.
* @returns {Promise<{status: number, body: any}>} - A promise that resolves to the response of the assignment operation.
* @throws {Error} - Throws an error if the HTTP request fails.
*/
public async assignPolicy(policyId: string, groupId: string, accessToken: string) {
try {
const url = `${this.graphBaseUrl}/deviceManagement/deviceCompliancePolicies/${policyId}/assign`;

const assignment = {
assignments: [
{
target: {
"@odata.type": "#microsoft.graph.groupAssignmentTarget",
groupId: groupId,
},
},
],
};

const config: AxiosRequestConfig = {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
};

const response = await axios.post(url, assignment, config);

return {
status: 201,
body: response.data,
};
} catch (error) {
console.error("Error assigning compliance policy:", error);
throw new Error("Failed to assign compliance policy");
}
}
}

Conclusion

Automating compliance policy management using Azure Functions and TypeScript not only simplifies operations but also enhances organizational security and compliance. With this implementation, businesses can maintain a secure and compliant environment with minimal effort.

References

Stackademic 🎓

Thank you for reading until the end. Before you go:


Automating Compliance Policy Assignments with Azure Functions and TypeScript was originally published in Stackademic on Medium, where people are continuing the conversation by highlighting and responding to this story.