Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ Agents that export telemetry through the delegated (OBO) route need `Agent365.Ob

Blueprint agents that export telemetry through the app-only S2S endpoint don't need these permissions, and `a365 setup all` no longer requests them for blueprint agents (#501).

#### Existing agents: grant Defender API permissions

Agents provisioned before this release need `RealtimeProtection.Evaluate.All` granted as both a **delegated** and an **application** permission on the blueprint app for the Defender security integration. Requires Global Administrator. Follow the steps above, searching for `86a21212-634e-4553-b3d6-e477e4c9d9ec` in step 2 and selecting `RealtimeProtection.Evaluate.All` in steps 3 and 4. Re-running `a365 setup all` grants it automatically.

### Added
- `RealtimeProtection.Evaluate.All` on the Defender API is now granted automatically during `a365 setup` as both a delegated and an application permission, enabling the Microsoft Defender security integration without manual Entra steps.
Comment on lines +33 to +36
- `a365 develop-mcp grant-agents-access --agent-blueprint-id <GUID> --mcp-server-name <NAME>` reports which agent instances of a blueprint are missing the permission to call a BYO MCP server, and prompts you to select which ones to grant it to (#500).
- When more than one Entra application shares the MCP server's name, `a365 develop-mcp grant-agents-access` now lists them all and asks which one to use instead of failing (#500).
- `a365 develop-mcp grant-agents-access --help` now lists Microsoft's first-party agent blueprint names and IDs, and the same list is printed when `--agent-blueprint-id` is missing or not a GUID, so you can find the ID without looking it up elsewhere (#500).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@ private static Command CreateInstanceScopesSubcommand(
null or "" => null,
AuthenticationConstants.MicrosoftGraphResourceAppId => "Microsoft Graph",
ConfigConstants.MessagingBotApiAppId => "Messaging Bot API",
ConfigConstants.DefenderApiAppId => "Defender API",
PowerPlatformConstants.PowerPlatformApiResourceAppId => "Power Platform API",
"00000002-0000-0000-c000-000000000000" => "Azure Active Directory Graph",
"797f4846-ba00-4fd7-ba43-dac1f8f63013" => "Azure Service Management",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace Microsoft.Agents.A365.DevTools.Cli.Commands.SetupSubcommands;
/// 1. Requirements validation
/// 2. Blueprint creation (shared with DW)
/// 3. Batch permissions on the blueprint (shared with DW pipeline; non-DW spec set:
/// Power Platform API and custom; Observability API is not requested). MAC reads
/// Defender API, Power Platform API, custom, and optionally Observability API). MAC reads
/// from the blueprint, so stamping here gives the same set visibility there.
/// 4. Agent Identity creation via POST /beta/servicePrincipals/Microsoft.Graph.AgentIdentity
/// 5. Agent Identity permission grants (same spec set as step 3) — OBO or S2S
Expand Down Expand Up @@ -126,15 +126,16 @@ public static void PrintDryRunPlan(Agent365Config config, ILogger logger, bool i
logger.LogInformation(sub + "create managed identity");
}

// 3. Inheritable Permissions — non-DW spec set (Power Platform API and custom; Observability API is
// not requested) stamped on the blueprint via SetInheritablePermissionsAsync so MAC and other
// dependent systems can see them. The same set is applied to the agent identity SP in step 5.
// 3. Inheritable Permissions — the non-DW spec set is stamped on the blueprint so MAC and
// dependent systems can see it. The same set is applied to the agent identity SP in step 5.
var selectedAuthMode = authMode ?? config.AuthMode;
var effectiveMode = string.IsNullOrWhiteSpace(selectedAuthMode)
? "obo"
: selectedAuthMode.Trim().ToLowerInvariant();
logger.LogInformation(SetupHelpers.DryRunRow(3, "Inheritable Permissions") + "configure for {Resources} (Global Administrator required; consent URL printed if absent)",
skipObservabilityPermissions ? "Power Platform API and custom permissions" : "Observability API, Power Platform API, and custom permissions");
skipObservabilityPermissions
? "Defender API, Power Platform API, and custom permissions"
: "Observability API, Defender API, Power Platform API, and custom permissions");
if (observabilityPermissionsEffectivelySkipped)
logger.LogInformation(sub + "Observability API not requested (registered agents export telemetry with an app-only token)");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ private static Command CreateBotSubcommand(
logger.LogInformation(" - Blueprint: {BlueprintId}", dryRunConfig.AgentBlueprintId);
logger.LogInformation(" - Messaging Bot API: {Scope}", ConfigConstants.MessagingBotApiAdminConsentScope);
logger.LogInformation(" - Observability API: {OtelScope} (delegated + application)", ConfigConstants.ObservabilityApiOtelWriteScope);
logger.LogInformation(" - Defender API: {DefenderScope} (delegated + application)", ConfigConstants.DefenderApiRealtimeProtectionScope);
logger.LogInformation(" - Power Platform API: Connectivity.Connections.Read");
logger.LogInformation("No changes made. Run without --dry-run to execute.");
return;
Expand Down Expand Up @@ -848,6 +849,7 @@ internal static async Task RemoveStaleCustomPermissionsAsync(
envAtgAppId,
ConfigConstants.MessagingBotApiAppId,
observabilityAppId,
ConfigConstants.DefenderApiAppId,
PowerPlatformConstants.PowerPlatformApiResourceAppId,
AuthenticationConstants.MicrosoftGraphResourceAppId,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,10 @@ internal static void PrintDryRunBlueprintReuseRows(ILogger logger, string bluepr
/// Returns the fixed-scope ResourcePermissionSpecs for the platform APIs that every
/// agent blueprint requires.
/// <para>
/// Power Platform API is always included. Observability API, using the app ID for
/// Defender API and Power Platform API are always included. Observability API, using the app ID for
/// <paramref name="environment"/>'s cloud, is included unless <paramref name="includeObservability"/>
/// is false. Messaging Bot API is
/// included only when <paramref name="isM365"/> is true — non-M365 (blueprint-only) agents
/// have no messaging surface so Bot scopes serve no purpose.
/// is false. Messaging Bot API is included only when <paramref name="isM365"/> is true —
/// non-M365 (blueprint-only) agents have no messaging surface so Bot scopes serve no purpose.
/// </para>
/// </summary>
internal static ResourcePermissionSpec[] GetFixedApiPermissionSpecs(
Expand Down Expand Up @@ -88,6 +87,12 @@ internal static ResourcePermissionSpec[] GetFixedApiPermissionSpecs(
setInheritable,
AppRoleScopes: new[] { ConfigConstants.ObservabilityApiOtelWriteScope }));
}
specs.Add(new ResourcePermissionSpec(
ConfigConstants.DefenderApiAppId,
"Defender API",
new[] { ConfigConstants.DefenderApiRealtimeProtectionScope },
setInheritable,
AppRoleScopes: new[] { ConfigConstants.DefenderApiRealtimeProtectionScope }));
specs.Add(new ResourcePermissionSpec(
PowerPlatformConstants.PowerPlatformApiResourceAppId,
"Power Platform API",
Expand Down Expand Up @@ -470,8 +475,8 @@ internal static async Task<string> ResolveBootstrapEnvironmentAsync(

/// <summary>
/// Fixed permission specs for the non-DW admin consent flow.
/// Observability API requires both Application (app role for S2S) and Delegated (oauth2 grant for OBO).
/// Power Platform API requires Delegated only.
/// Observability API and Defender API require both Application (app role for S2S)
/// and Delegated (oauth2 grant for OBO). Power Platform API requires Delegated only.
/// Extend this list or pass an override to <see cref="LogNonDwAdminConsentInstructions"/>
/// when additional APIs are required (e.g. dynamic MCP scopes, custom permissions).
/// </summary>
Expand All @@ -489,15 +494,27 @@ internal static async Task<string> ResolveBootstrapEnvironmentAsync(
[
("Observability API", observabilityAppId, ConfigConstants.ObservabilityApiOtelWriteScope, "Application"),
("Observability API", observabilityAppId, ConfigConstants.ObservabilityApiOtelWriteScope, "Delegated"),
("Defender API", ConfigConstants.DefenderApiAppId, ConfigConstants.DefenderApiRealtimeProtectionScope, "Application"),
("Defender API", ConfigConstants.DefenderApiAppId, ConfigConstants.DefenderApiRealtimeProtectionScope, "Delegated"),
("Power Platform API", PowerPlatformConstants.PowerPlatformApiResourceAppId, PowerPlatformConstants.PermissionNames.ConnectivityConnectionsRead, "Delegated"),
];
}

/// <summary>
/// Fixed platform APIs that expose an application (S2S) app role, used to render the manual
/// PowerShell hand-off when the programmatic assignment could not complete.
/// </summary>
internal static readonly IReadOnlyList<(string ResourceName, string ResourceAppId, string Role)> FixedApiAppRoleHandoffSpecs =
[
("Observability API", ConfigConstants.ObservabilityApiAppId, ConfigConstants.ObservabilityApiOtelWriteScope),
("Defender API", ConfigConstants.DefenderApiAppId, ConfigConstants.DefenderApiRealtimeProtectionScope),
];

Comment on lines +508 to +512
/// <summary>
/// Logs step-by-step instructions for a Global Administrator to grant admin consent
/// for the blueprint app, with two options: Entra portal and PowerShell.
/// <para>
/// Defaults to <see cref="NonDwAdminConsentSpecs"/> (Observability API + Power Platform API).
/// Defaults to <see cref="NonDwAdminConsentSpecs"/> (Observability, Defender, and Power Platform APIs).
/// Pass an explicit <paramref name="specs"/> list to support dynamic or extended permission sets.
/// </para>
/// </summary>
Expand Down Expand Up @@ -990,7 +1007,6 @@ public static void DisplaySetupSummary(SetupResults results, ILogger logger, str
s2sTargets.Add((true, results.PendingAgentIdentityAppRoleSpecs.Where(spec => spec.AppRoleScopes is { Length: > 0 }).Distinct().ToList()));
if (blueprintS2sFailed)
s2sTargets.Add((false, results.PendingBlueprintAppRoleSpecs.Where(spec => spec.AppRoleScopes is { Length: > 0 }).Distinct().ToList()));

foreach (var (isAgentIdentityTarget, pendingAppRoleSpecs) in s2sTargets)
{
actionCount++;
Expand Down Expand Up @@ -1074,6 +1090,11 @@ public static void DisplaySetupSummary(SetupResults results, ILogger logger, str
logger.LogInformation(" Invoke-MgGraphRequest -Method POST -Uri '{GraphBaseUrl}/v1.0/oauth2PermissionGrants' -Body $body -ContentType 'application/json'", resolvedGraphBaseUrl);
logger.LogInformation("");
}
logger.LogInformation(" # Defender API");
logger.LogInformation(" $defenderSp = Get-MgServicePrincipal -Filter \"appId eq '{DefenderAppId}'\"", ConfigConstants.DefenderApiAppId);
logger.LogInformation(" $body = @{{ clientId = $agentSpId; consentType = 'AllPrincipals'; resourceId = $defenderSp.Id; scope = '{DefenderScope}' }} | ConvertTo-Json", ConfigConstants.DefenderApiRealtimeProtectionScope);
logger.LogInformation(" Invoke-MgGraphRequest -Method POST -Uri '{GraphBaseUrl}/v1.0/oauth2PermissionGrants' -Body $body -ContentType 'application/json'", resolvedGraphBaseUrl);
logger.LogInformation("");
logger.LogInformation(" # Power Platform API");
logger.LogInformation(" $ppSp = Get-MgServicePrincipal -Filter \"appId eq '{PpAppId}'\"", PowerPlatformConstants.PowerPlatformApiResourceAppId);
logger.LogInformation(" $body = @{{ clientId = $agentSpId; consentType = 'AllPrincipals'; resourceId = $ppSp.Id; scope = '{PpScope}' }} | ConvertTo-Json", PowerPlatformConstants.PermissionNames.ConnectivityConnectionsRead);
Expand Down Expand Up @@ -1280,6 +1301,7 @@ internal static List<string> PopulateAdminConsentUrls(
["Agent 365 Tools"] = mcpResourceAppId,
["Messaging Bot API"] = ConfigConstants.MessagingBotApiAppId,
["Observability API"] = observabilityResourceAppId,
["Defender API"] = ConfigConstants.DefenderApiAppId,
["Power Platform API"] = PowerPlatformConstants.PowerPlatformApiResourceAppId,
};

Expand Down Expand Up @@ -1387,6 +1409,8 @@ internal static string GetResourceIdentifierUri(
return ConfigConstants.MessagingBotApiIdentifierUri;
if (ConfigConstants.IsObservabilityApiAppId(resourceAppId))
return ConfigConstants.BuildObservabilityApiIdentifierUri(resourceAppId);
if (string.Equals(resourceAppId, ConfigConstants.DefenderApiAppId, StringComparison.OrdinalIgnoreCase))
return ConfigConstants.DefenderApiIdentifierUri;
if (string.Equals(resourceAppId, PowerPlatformConstants.PowerPlatformApiResourceAppId, StringComparison.OrdinalIgnoreCase))
return PowerPlatformConstants.PowerPlatformApiIdentifierUri;
// WorkIQ Tools shared (issue #429): match by appId, not display name. V2 per-server
Expand Down Expand Up @@ -1534,6 +1558,7 @@ string Build(string tenant, string client, string resourceUri, IEnumerable<strin
observabilityResourceAppId ?? ConfigConstants.ObservabilityApiAppId);
urls.Add(("Observability API", Build(tenantId, blueprintClientId, observabilityIdentifierUri, new[] { ConfigConstants.ObservabilityApiOtelWriteScope })));
}
urls.Add(("Defender API", Build(tenantId, blueprintClientId, ConfigConstants.DefenderApiIdentifierUri, new[] { ConfigConstants.DefenderApiRealtimeProtectionScope })));
urls.Add(("Power Platform API", Build(tenantId, blueprintClientId, PowerPlatformConstants.PowerPlatformApiIdentifierUri, new[] { PowerPlatformConstants.PermissionNames.ConnectivityConnectionsRead })));

return urls;
Expand Down Expand Up @@ -1598,6 +1623,7 @@ internal static string BuildCombinedConsentUrl(
if (includeObservability)
allScopes.Add(
$"{ConfigConstants.BuildObservabilityApiIdentifierUri(observabilityResourceAppId ?? ConfigConstants.ObservabilityApiAppId)}/{ConfigConstants.ObservabilityApiOtelWriteScope}");
allScopes.Add($"{ConfigConstants.DefenderApiIdentifierUri}/{ConfigConstants.DefenderApiRealtimeProtectionScope}");
allScopes.Add($"{PowerPlatformConstants.PowerPlatformApiIdentifierUri}/{PowerPlatformConstants.PermissionNames.ConnectivityConnectionsRead}");
return BuildAdminConsentUrl(tenantId, blueprintClientId, allScopes, authorityHost);
}
Expand Down Expand Up @@ -1704,7 +1730,7 @@ internal static void PrintDwSetupAllDryRunPlan(
}

// 4. Inheritable Permissions
logger.LogInformation(DryRunRow(4, "Inheritable Permissions") + "configure for Microsoft Graph, Agent 365 Tools, Messaging Bot API, Observability API, Power Platform API");
logger.LogInformation(DryRunRow(4, "Inheritable Permissions") + "configure for Microsoft Graph, Agent 365 Tools, Messaging Bot API, Observability API, Defender API, Power Platform API");

// 5. Blueprint Permission Grants
logger.LogInformation(DryRunRow(5, "Blueprint Permission Grants") + "admin approval required — see 'Action Required' in setup output");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ public static class ConfigConstants
/// </summary>
public const string ObservabilityApiIdentifierUri = "api://9b975845-388f-4429-889e-eab1ef63949c";

/// <summary>
/// Defender API App ID
/// </summary>
public const string DefenderApiAppId = "86a21212-634e-4553-b3d6-e477e4c9d9ec";

/// <summary>
/// Defender API identifier URI.
/// </summary>
public const string DefenderApiIdentifierUri = "api://86a21212-634e-4553-b3d6-e477e4c9d9ec";

/// <summary>
/// Single source of truth for the Messaging Bot API delegated scope.
/// The resource SP (appId 5a807f24-c9de-44ee-a3a7-329e88a00ffc) exposes exactly
Expand All @@ -125,6 +135,12 @@ public static class ConfigConstants
/// </summary>
public const string ObservabilityApiOtelWriteScope = "Agent365.Observability.OtelWrite";

/// <summary>
/// Defender API app role and delegated scope for the Defender security integration.
/// Must match the value published on the resource SP.
/// </summary>
public const string DefenderApiRealtimeProtectionScope = "RealtimeProtection.Evaluate.All";

/// <summary>
/// Delegated scope value exposed on the blueprint app registration to enable
/// OBO (On-Behalf-Of) callers to acquire tokens scoped to the agent.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public sealed class LogRedactionService : ILogRedactionService
"00000003-0000-0000-c000-000000000000", // Microsoft Graph
"5a807f24-c9de-44ee-a3a7-329e88a00ffc", // Agent 365 Messaging Bot API
"9b975845-388f-4429-889e-eab1ef63949c", // Agent 365 Observability API
"86a21212-634e-4553-b3d6-e477e4c9d9ec", // Agent 365 Defender API
ConfigConstants.GccObservabilityApiAppId,
ConfigConstants.GccHighObservabilityApiAppId,
ConfigConstants.DodObservabilityApiAppId,
Expand Down
Loading
Loading