ownerlens 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/LICENSE +183 -0
  2. package/README.md +209 -0
  3. package/bin/ownerlens.js +92 -0
  4. package/dist/assets/index-B9aAYpVl.css +1 -0
  5. package/dist/assets/index-BcwLk2bx.js +10 -0
  6. package/dist/index.html +13 -0
  7. package/package.json +73 -0
  8. package/src/App.tsx +18 -0
  9. package/src/components/azure/AzureComponent.test.tsx +625 -0
  10. package/src/components/azure/AzureComponent.tsx +189 -0
  11. package/src/components/azure/AzureRbacComponent.tsx +104 -0
  12. package/src/components/azure/ClosableAzureTab.tsx +42 -0
  13. package/src/components/azure/EntraPermissionsComponent.tsx +194 -0
  14. package/src/components/azure/ManagedIdentityComponent.test.tsx +324 -0
  15. package/src/components/azure/ManagedIdentityComponent.tsx +141 -0
  16. package/src/components/azure/ResourceGroupComponent.tsx +157 -0
  17. package/src/components/azure/ServicePrincipalComponent.test.tsx +457 -0
  18. package/src/components/azure/ServicePrincipalComponent.tsx +155 -0
  19. package/src/components/azure/ServicePrincipalFieldRenderers.tsx +140 -0
  20. package/src/components/azure/ZtaComponent.test.tsx +267 -0
  21. package/src/components/azure/ZtaComponent.tsx +276 -0
  22. package/src/components/azure/ZtaRemediationBadge.tsx +70 -0
  23. package/src/components/azure/api.ts +216 -0
  24. package/src/components/azure/azureReportConfig.ts +247 -0
  25. package/src/core/azure/azureRbac.ts +70 -0
  26. package/src/core/azure/entra/index.ts +1 -0
  27. package/src/core/azure/entra/managedIdentity.ts +21 -0
  28. package/src/core/azure/entra/servicePrincipal.ts +34 -0
  29. package/src/core/azure/entra/types.ts +56 -0
  30. package/src/core/azure/identityEnrichment.ts +65 -0
  31. package/src/core/azure/resources.ts +141 -0
  32. package/src/core/azure/ztaReport.ts +58 -0
  33. package/src/core/config.ts +39 -0
  34. package/src/core/ownership/OwnershipTarget.ts +32 -0
  35. package/src/core/ownership/resolveOwner.ts +5 -0
  36. package/src/core/ownership/types.ts +14 -0
  37. package/src/core/risk/types.ts +1 -0
  38. package/src/core/runtime/index.ts +1 -0
  39. package/src/core/runtime/localSnapshotFiles.ts +74 -0
  40. package/src/core/runtime/rest.ts +61 -0
  41. package/src/lib/searchFilterUtils.ts +17 -0
  42. package/src/lib/utils.ts +48 -0
  43. package/src/main.tsx +10 -0
  44. package/src/providers/azure/identities/azureIdentityTypes.ts +1 -0
  45. package/src/providers/azure/identities/buildAzureManagedIdentityAssignmentIndex.test.ts +32 -0
  46. package/src/providers/azure/identities/buildAzureManagedIdentityAssignmentIndex.ts +35 -0
  47. package/src/providers/azure/identities/userAssignedIdentityAssignments.ts +52 -0
  48. package/src/providers/azure/inputTransferObject/entra/EntraAppRoleAssignment.ts +10 -0
  49. package/src/providers/azure/inputTransferObject/entra/EntraApplication.ts +27 -0
  50. package/src/providers/azure/inputTransferObject/entra/EntraOAuth2PermissionGrant.ts +8 -0
  51. package/src/providers/azure/inputTransferObject/entra/EntraServicePrincipal.ts +43 -0
  52. package/src/providers/azure/inputTransferObject/entra/EntraSnapshot.ts +13 -0
  53. package/src/providers/azure/inputTransferObject/entra/EntraSnapshotMeta.ts +12 -0
  54. package/src/providers/azure/inputTransferObject/resources/AzureActivityLog.ts +1 -0
  55. package/src/providers/azure/inputTransferObject/resources/AzureResource.ts +1 -0
  56. package/src/providers/azure/inputTransferObject/resources/AzureResourceGroup.ts +1 -0
  57. package/src/providers/azure/inputTransferObject/resources/AzureRoleAssignment.ts +1 -0
  58. package/src/providers/azure/inputTransferObject/resources/AzureSnapshot.ts +1 -0
  59. package/src/providers/azure/inputTransferObject/resources/AzureSnapshotMeta.ts +1 -0
  60. package/src/providers/azure/inputTransferObject/resources/AzureSubscription.ts +1 -0
  61. package/src/providers/azure/inputTransferObject/resources/AzureUserAssignedManagedIdentity.ts +1 -0
  62. package/src/providers/azure/ownership/azureActivityOwnershipEvidence.ts +60 -0
  63. package/src/providers/azure/ownership/azureOwnerReportTypes.ts +13 -0
  64. package/src/providers/azure/ownership/azureOwnershipConfig.ts +21 -0
  65. package/src/providers/azure/ownership/azureOwnershipTypes.ts +46 -0
  66. package/src/providers/azure/ownership/buildAzureOwnershipReport.test.ts +99 -0
  67. package/src/providers/azure/ownership/buildAzureOwnershipReport.ts +90 -0
  68. package/src/providers/azure/ownership/buildAzureOwnershipTargets.test.ts +87 -0
  69. package/src/providers/azure/ownership/buildAzureOwnershipTargets.ts +42 -0
  70. package/src/providers/azure/ownership/resolveAzureOwner.ts +146 -0
  71. package/src/providers/azure/runtime/DisabledEvidenceStore.ts +34 -0
  72. package/src/providers/azure/runtime/EnrichmentService.ts +35 -0
  73. package/src/providers/azure/runtime/LocalReportRuntime.test.ts +2318 -0
  74. package/src/providers/azure/runtime/LocalReportRuntime.ts +302 -0
  75. package/src/providers/azure/runtime/RuntimeHost.ts +60 -0
  76. package/src/providers/azure/runtime/SnapshotImporter.ts +44 -0
  77. package/src/providers/azure/runtime/enrichment/azureIdentityEnrichment.ts +523 -0
  78. package/src/providers/azure/runtime/enrichment/azureScopeClassifier.ts +30 -0
  79. package/src/providers/azure/runtime/enrichment/evaluateAzureRoleAssignmentRisk.ts +88 -0
  80. package/src/providers/azure/runtime/entra/EntraCollectionQueryService.ts +307 -0
  81. package/src/providers/azure/runtime/entra/LocalEntraReportRuntime.ts +227 -0
  82. package/src/providers/azure/runtime/entra/appRoleAssignmentsTable.ts +52 -0
  83. package/src/providers/azure/runtime/entra/applicationsTable.ts +175 -0
  84. package/src/providers/azure/runtime/entra/entraServicePrincipalMapper.ts +63 -0
  85. package/src/providers/azure/runtime/entra/localReportRuntimeRest.ts +41 -0
  86. package/src/providers/azure/runtime/entra/oauth2PermissionGrantsTable.ts +48 -0
  87. package/src/providers/azure/runtime/entra/principalProjection.ts +173 -0
  88. package/src/providers/azure/runtime/entra/servicePrincipalsTable.ts +149 -0
  89. package/src/providers/azure/runtime/entra/snapshotMetadataTable.ts +18 -0
  90. package/src/providers/azure/runtime/entra/snapshotStore.ts +102 -0
  91. package/src/providers/azure/runtime/localReportCollections.ts +101 -0
  92. package/src/providers/azure/runtime/localReportRuntimeRest.ts +71 -0
  93. package/src/providers/azure/runtime/resources/AzureResourcesCollectionQueryService.ts +145 -0
  94. package/src/providers/azure/runtime/resources/LocalAzureResourcesReportRuntime.ts +114 -0
  95. package/src/providers/azure/runtime/resources/disabledOwnerEvidenceTable.ts +60 -0
  96. package/src/providers/azure/runtime/resources/localReportRuntimeRest.ts +81 -0
  97. package/src/providers/azure/runtime/resources/resourceGroupOwnership.ts +90 -0
  98. package/src/providers/azure/runtime/resources/snapshotMetadataTable.ts +19 -0
  99. package/src/providers/azure/runtime/resources/snapshotStore.ts +128 -0
  100. package/src/providers/azure/runtime/resources/tables.ts +441 -0
  101. package/src/providers/azure/runtime/runtimeRestQuery.ts +46 -0
  102. package/src/providers/azure/runtime/runtimeSqlSchema.ts +357 -0
  103. package/src/providers/azure/runtime/zta/Discovery.ts +141 -0
  104. package/src/providers/azure/runtime/zta/LocalZeroTrustAssessmentReportRuntime.ts +86 -0
  105. package/src/providers/azure/runtime/zta/ZeroTrustAssessmentQueryService.ts +124 -0
  106. package/src/providers/azure/runtime/zta/localReportRuntimeRest.ts +15 -0
  107. package/src/providers/azure/runtime/zta/snapshotMetadataTable.ts +77 -0
  108. package/src/providers/azure/runtime/zta/snapshotStore.ts +112 -0
  109. package/src/providers/azure/runtime/zta/tables.ts +361 -0
  110. package/src/providers/azure/runtime/zta/types.ts +7 -0
  111. package/src/providers/azure/runtime/zta/ztaReportMapper.ts +12 -0
  112. package/src/report/applyCollectionControls.ts +289 -0
  113. package/src/report/buildCollectionColumns.tsx +38 -0
  114. package/src/report/components/ConfidenceBadge.tsx +10 -0
  115. package/src/report/components/EvidenceList.test.ts +25 -0
  116. package/src/report/components/EvidenceList.tsx +52 -0
  117. package/src/report/components/GenericTable.tsx +373 -0
  118. package/src/report/components/PermissionRiskBadge.tsx +19 -0
  119. package/src/report/components/reportTableControls.test.ts +175 -0
  120. package/src/report/components/reportTableControls.tsx +483 -0
  121. package/src/report/components/ui/badge.tsx +35 -0
  122. package/src/report/components/ui/button.tsx +38 -0
  123. package/src/report/components/ui/card.tsx +23 -0
  124. package/src/report/components/ui/input.tsx +15 -0
  125. package/src/report/components/ui/table.tsx +44 -0
  126. package/src/report/components/ui/tabs.tsx +29 -0
  127. package/src/report/export/csv.ts +34 -0
  128. package/src/report/ownerManualPrecheck.test.ts +137 -0
  129. package/src/report/ownerManualPrecheck.ts +132 -0
  130. package/src/report/reportArchitecture.test.ts +125 -0
  131. package/src/report/reportTypes.ts +54 -0
  132. package/src/report/reportValueRenderers.tsx +54 -0
  133. package/src/report/runtimeCollectionQuery.ts +23 -0
  134. package/src/report/types.ts +14 -0
  135. package/src/styles.css +43 -0
  136. package/tools/README.md +108 -0
  137. package/tools/azure-activity-check.ps1 +164 -0
  138. package/tools/collect-azure.ps1 +54 -0
  139. package/tools/collect-entra.ps1 +47 -0
  140. package/tools/collect-scripts.test.ts +22 -0
  141. package/tools/prepare-entra-snapshot.ps1 +403 -0
  142. package/tools/prepare-entra-snapshot.test.ts +14 -0
  143. package/tools/prepare-resource-snapshot.ps1 +345 -0
  144. package/vite.config.ts +23 -0
@@ -0,0 +1,35 @@
1
+ import type { AzureResource } from "../../../core/azure/resources";
2
+ import type { AzureManagedIdentityResourceAssignment } from "./azureIdentityTypes";
3
+ import { normalizeUserAssignedIdentityAssignments } from "./userAssignedIdentityAssignments";
4
+
5
+ export function getResourceManagedIdentityAssignments(resource: AzureResource): AzureManagedIdentityResourceAssignment[] {
6
+ const assignments = normalizeUserAssignedIdentityAssignments(resource.userAssignedIdentities).map((assignment) => ({
7
+ ...assignment,
8
+ assignedResourceId: resource.resourceId,
9
+ assignedResourceName: resource.resourceName,
10
+ assignedResourceType: resource.resourceType,
11
+ assignedResourceGroup: resource.resourceGroup,
12
+ subscriptionId: resource.subscriptionId,
13
+ subscriptionName: resource.subscriptionName
14
+ }));
15
+
16
+ if (hasSystemAssignedIdentity(resource)) {
17
+ assignments.push({
18
+ resourceId: resource.resourceId,
19
+ clientId: null,
20
+ principalId: resource.identityPrincipalId,
21
+ assignedResourceId: resource.resourceId,
22
+ assignedResourceName: resource.resourceName,
23
+ assignedResourceType: resource.resourceType,
24
+ assignedResourceGroup: resource.resourceGroup,
25
+ subscriptionId: resource.subscriptionId,
26
+ subscriptionName: resource.subscriptionName
27
+ });
28
+ }
29
+
30
+ return assignments;
31
+ }
32
+
33
+ function hasSystemAssignedIdentity(resource: AzureResource): boolean {
34
+ return Boolean(resource.identityPrincipalId && resource.identityType?.toLowerCase().includes("systemassigned"));
35
+ }
@@ -0,0 +1,52 @@
1
+ import type { AzureUserAssignedIdentityAssignment } from "../../../core/azure/resources";
2
+
3
+ export function normalizeUserAssignedIdentityAssignments(
4
+ userAssignedIdentities: unknown
5
+ ): AzureUserAssignedIdentityAssignment[] {
6
+ if (!isRecord(userAssignedIdentities)) {
7
+ return Array.isArray(userAssignedIdentities)
8
+ ? userAssignedIdentities.map(normalizeExistingAssignment).filter(isAssignment)
9
+ : [];
10
+ }
11
+
12
+ return Object.entries(userAssignedIdentities).map(([resourceId, details]) => ({
13
+ resourceId,
14
+ clientId: getStringProperty(details, "clientId"),
15
+ principalId: getStringProperty(details, "principalId")
16
+ }));
17
+ }
18
+
19
+ function normalizeExistingAssignment(value: unknown): AzureUserAssignedIdentityAssignment | null {
20
+ if (!isRecord(value)) {
21
+ return null;
22
+ }
23
+
24
+ const resourceId = getStringProperty(value, "resourceId");
25
+ if (!resourceId) {
26
+ return null;
27
+ }
28
+
29
+ return {
30
+ resourceId,
31
+ clientId: getStringProperty(value, "clientId"),
32
+ principalId: getStringProperty(value, "principalId")
33
+ };
34
+ }
35
+
36
+ function isAssignment(value: AzureUserAssignedIdentityAssignment | null): value is AzureUserAssignedIdentityAssignment {
37
+ return value !== null;
38
+ }
39
+
40
+ function getStringProperty(value: unknown, propertyName: string): string | null {
41
+ if (!isRecord(value)) {
42
+ return null;
43
+ }
44
+
45
+ const matchingKey = Object.keys(value).find((key) => key.toLowerCase() === propertyName.toLowerCase());
46
+ const propertyValue = matchingKey ? value[matchingKey] : null;
47
+ return typeof propertyValue === "string" && propertyValue.trim() ? propertyValue.trim() : null;
48
+ }
49
+
50
+ function isRecord(value: unknown): value is Record<string, unknown> {
51
+ return typeof value === "object" && value !== null && !Array.isArray(value);
52
+ }
@@ -0,0 +1,10 @@
1
+ export type EntraAppRoleAssignment = {
2
+ id: string;
3
+ appRoleId: string;
4
+ appRoleDisplayName: string | null;
5
+ appRoleValue: string | null;
6
+ principalId: string;
7
+ principalDisplayName: string | null;
8
+ resourceId: string;
9
+ resourceDisplayName: string | null;
10
+ };
@@ -0,0 +1,27 @@
1
+ import type { EntraOwner, EntraAppRole } from "./EntraServicePrincipal";
2
+
3
+ export type EntraApplicationCredential = Record<string, unknown>;
4
+
5
+ export type EntraApplication = {
6
+ id: string;
7
+ appId: string;
8
+ displayName: string;
9
+ signInAudience: string | null;
10
+ publisherDomain: string | null;
11
+ identifierUris: string[];
12
+ tags: string[];
13
+ appRoles: EntraAppRole[];
14
+ oauth2PermissionScopes: Record<string, unknown>[];
15
+ requiredResourceAccess: Record<string, unknown>[];
16
+ web: Record<string, unknown> | null;
17
+ spa: Record<string, unknown> | null;
18
+ publicClient: Record<string, unknown> | null;
19
+ passwordCredentials: EntraApplicationCredential[];
20
+ keyCredentials: EntraApplicationCredential[];
21
+ createdDateTime: string | null;
22
+ deletedDateTime: string | null;
23
+ disabledByMicrosoftStatus: string | null;
24
+ info: Record<string, unknown> | null;
25
+ notes: string | null;
26
+ owners: EntraOwner[];
27
+ };
@@ -0,0 +1,8 @@
1
+ export type EntraOAuth2PermissionGrant = {
2
+ id: string;
3
+ clientId: string;
4
+ consentType: "Principal" | "AllPrincipals" | string;
5
+ principalId: string | null;
6
+ resourceId: string;
7
+ scope: string;
8
+ };
@@ -0,0 +1,43 @@
1
+ export type ServicePrincipalType = "Application" | "ManagedIdentity" | "SocialIdp" | "Legacy";
2
+
3
+ export type ServicePrincipalAppRole = {
4
+ id: string;
5
+ value: string | null;
6
+ displayName: string | null;
7
+ description: string | null;
8
+ isEnabled: boolean | null;
9
+ allowedMemberTypes: string[];
10
+ };
11
+
12
+ export type ServicePrincipalOwner = {
13
+ id?: string | null;
14
+ displayName?: string | null;
15
+ userPrincipalName?: string | null;
16
+ mail?: string | null;
17
+ ownerType?: string | null;
18
+ };
19
+
20
+ export type EntraServicePrincipal = {
21
+ id: string;
22
+ appId: string;
23
+ displayName: string;
24
+ appDisplayName: string | null;
25
+ servicePrincipalType: ServicePrincipalType;
26
+ publisherName: string | null;
27
+ accountEnabled: boolean;
28
+ appOwnerOrganizationId: string | null;
29
+ homepage: string | null;
30
+ loginUrl: string | null;
31
+ replyUrls: string[];
32
+ servicePrincipalNames: string[];
33
+ tags: string[];
34
+ appRoles?: ServicePrincipalAppRole[];
35
+ owners?: ServicePrincipalOwner[];
36
+ appOwners?: ServicePrincipalOwner[];
37
+ servicePrincipalOwners?: ServicePrincipalOwner[];
38
+ applicationOwners?: ServicePrincipalOwner[];
39
+ metadata?: Record<string, unknown> | null;
40
+ };
41
+
42
+ export type EntraAppRole = ServicePrincipalAppRole;
43
+ export type EntraOwner = ServicePrincipalOwner;
@@ -0,0 +1,13 @@
1
+ import type { EntraApplication } from "./EntraApplication";
2
+ import type { EntraAppRoleAssignment } from "./EntraAppRoleAssignment";
3
+ import type { EntraOAuth2PermissionGrant } from "./EntraOAuth2PermissionGrant";
4
+ import type { EntraServicePrincipal } from "./EntraServicePrincipal";
5
+ import type { EntraSnapshotMeta } from "./EntraSnapshotMeta";
6
+
7
+ export type EntraSnapshot = {
8
+ meta: EntraSnapshotMeta;
9
+ servicePrincipals: EntraServicePrincipal[];
10
+ applications?: EntraApplication[];
11
+ oauth2PermissionGrants?: EntraOAuth2PermissionGrant[];
12
+ appRoleAssignments?: EntraAppRoleAssignment[];
13
+ };
@@ -0,0 +1,12 @@
1
+ export type EntraSnapshotMeta = {
2
+ provider: "entra";
3
+ snapshotVersion: string;
4
+ createdAt: string;
5
+ tenantId: string;
6
+ account: string;
7
+ scopes: string[];
8
+ servicePrincipalCount: number;
9
+ applicationCount?: number;
10
+ oauth2PermissionGrantCount?: number;
11
+ appRoleAssignmentCount?: number;
12
+ };
@@ -0,0 +1 @@
1
+ export type { AzureActivityLog } from "../../../../core/azure/resources";
@@ -0,0 +1 @@
1
+ export type { AzureResource, AzureUserAssignedIdentityAssignment } from "../../../../core/azure/resources";
@@ -0,0 +1 @@
1
+ export type { AzureResourceGroup, AzureResourceTags } from "../../../../core/azure/resources";
@@ -0,0 +1 @@
1
+ export type { AzureRoleAssignment } from "../../../../core/azure/resources";
@@ -0,0 +1 @@
1
+ export type { AzureSnapshot } from "../../../../core/azure/resources";
@@ -0,0 +1 @@
1
+ export type { AzureSnapshotMeta } from "../../../../core/azure/resources";
@@ -0,0 +1 @@
1
+ export type { AzureSubscription, AzureSubscriptionState } from "../../../../core/azure/resources";
@@ -0,0 +1 @@
1
+ export type { AzureUserAssignedManagedIdentity } from "../../../../core/azure/resources";
@@ -0,0 +1,60 @@
1
+ import type { EntraServicePrincipal } from "../inputTransferObject/entra/EntraServicePrincipal";
2
+ import type { AzureActivityLog } from "../../../core/azure/resources";
3
+
4
+ export function normalizeOwner(value: string): string {
5
+ return value.trim().toLowerCase();
6
+ }
7
+
8
+ export function getTagValue(tags: Record<string, string> | null, key: string): string | null {
9
+ if (!tags) {
10
+ return null;
11
+ }
12
+
13
+ const matchingKey = Object.keys(tags).find((tagKey) => tagKey.toLowerCase() === key.toLowerCase());
14
+ const value = matchingKey ? tags[matchingKey] : null;
15
+ return value?.trim() ? value.trim() : null;
16
+ }
17
+
18
+ export function getActivityIndexKey(subscriptionId: string, resourceGroupName: string | null): string {
19
+ return `${subscriptionId.toLowerCase()}::${(resourceGroupName ?? "").toLowerCase()}`;
20
+ }
21
+
22
+ export function isOwnerActivity(log: AzureActivityLog): boolean {
23
+ if (log.category !== "Administrative" || log.status !== "Succeeded" || !log.caller?.trim()) {
24
+ return false;
25
+ }
26
+
27
+ const action = `${log.authorizationAction ?? ""} ${log.operationNameValue ?? ""}`.toLowerCase();
28
+ return action.includes("/write") || action.includes("/action");
29
+ }
30
+
31
+ export function compareLogsNewestFirst(left: AzureActivityLog, right: AzureActivityLog): number {
32
+ return getTime(right.eventTimestamp) - getTime(left.eventTimestamp);
33
+ }
34
+
35
+ export function getTime(value: string): number {
36
+ const parsed = new Date(value).getTime();
37
+ return Number.isNaN(parsed) ? 0 : parsed;
38
+ }
39
+
40
+ export function buildServicePrincipalIndex(servicePrincipals: EntraServicePrincipal[]): Map<string, EntraServicePrincipal> {
41
+ const index = new Map<string, EntraServicePrincipal>();
42
+
43
+ for (const servicePrincipal of servicePrincipals) {
44
+ index.set(servicePrincipal.id.toLowerCase(), servicePrincipal);
45
+ index.set(servicePrincipal.appId.toLowerCase(), servicePrincipal);
46
+ }
47
+
48
+ return index;
49
+ }
50
+
51
+ export function describeIdentity(identity: string, servicePrincipalIndex: Map<string, EntraServicePrincipal>): string {
52
+ const normalized = normalizeOwner(identity);
53
+ const servicePrincipal = servicePrincipalIndex.get(normalized);
54
+
55
+ if (!servicePrincipal) {
56
+ return normalized;
57
+ }
58
+
59
+ return `${servicePrincipal.displayName} (${normalized})`;
60
+ }
@@ -0,0 +1,13 @@
1
+ import type { OwnerResolution } from "../../../core/ownership/types";
2
+
3
+ export type OwnerReportRow = OwnerResolution & {
4
+ kind: "subscription" | "resourceGroup";
5
+ resourceGroup: string | null;
6
+ subscriptionId: string;
7
+ subscriptionName: string;
8
+ targetKey: string;
9
+ };
10
+
11
+ export type OwnerReport = {
12
+ owners: OwnerReportRow[];
13
+ };
@@ -0,0 +1,21 @@
1
+ import { appConfig } from "../../../core/config";
2
+ import { azureOwnerAdapter } from "./resolveAzureOwner";
3
+ import type { AzureOwnerTagConfigMap, AzureReportConfig } from "./azureOwnershipTypes";
4
+
5
+ export const azureOwnershipConfig: AzureReportConfig = {
6
+ tags: buildOwnerTagConfigMap(appConfig.azure.ownership.ownerTags),
7
+ ownerTargets: [
8
+ {
9
+ kind: "subscription",
10
+ adapter: azureOwnerAdapter
11
+ },
12
+ {
13
+ kind: "resourceGroup",
14
+ adapter: azureOwnerAdapter
15
+ }
16
+ ]
17
+ };
18
+
19
+ function buildOwnerTagConfigMap(ownerTags: typeof appConfig.azure.ownership.ownerTags): AzureOwnerTagConfigMap {
20
+ return Object.fromEntries(ownerTags.map(({ name, confidence }) => [name, { confidence }]));
21
+ }
@@ -0,0 +1,46 @@
1
+ import type {
2
+ AzureActivityLog,
3
+ AzureResourceGroup,
4
+ AzureSnapshot,
5
+ AzureSubscription
6
+ } from "../../../core/azure/resources";
7
+ import type { OwnerResolver } from "../../../core/ownership/resolveOwner";
8
+ import type { OwnerResolution } from "../../../core/ownership/types";
9
+ import type { EntraSnapshot } from "../inputTransferObject/entra/EntraSnapshot";
10
+ import type { EntraServicePrincipal } from "../inputTransferObject/entra/EntraServicePrincipal";
11
+
12
+ export type AzureScopeOwnershipTarget =
13
+ | {
14
+ kind: "subscription";
15
+ subscription: AzureSubscription;
16
+ }
17
+ | {
18
+ kind: "resourceGroup";
19
+ resourceGroup: AzureResourceGroup;
20
+ };
21
+
22
+ export type OwnerResolverContext = {
23
+ resourceSnapshot: AzureSnapshot;
24
+ entraSnapshot: EntraSnapshot;
25
+ tags: AzureOwnerTagConfigMap;
26
+ activityLogIndex: ActivityLogIndex;
27
+ servicePrincipalIndex: Map<string, EntraServicePrincipal>;
28
+ };
29
+
30
+ export type OwnerResolverAdapter = OwnerResolver<AzureScopeOwnershipTarget, OwnerResolverContext>;
31
+
32
+ export type AzureOwnerTargetConfig = {
33
+ kind: AzureScopeOwnershipTarget["kind"];
34
+ adapter: OwnerResolverAdapter;
35
+ };
36
+
37
+ export type AzureReportConfig = {
38
+ tags: AzureOwnerTagConfigMap;
39
+ ownerTargets: AzureOwnerTargetConfig[];
40
+ };
41
+
42
+ export type ActivityLogIndex = Map<string, AzureActivityLog[]>;
43
+
44
+ export type AzureOwnerTagConfig = Pick<OwnerResolution, "confidence">;
45
+
46
+ export type AzureOwnerTagConfigMap = Record<string, AzureOwnerTagConfig>;
@@ -0,0 +1,99 @@
1
+ import { buildAzureOwnershipReport } from "./buildAzureOwnershipReport";
2
+ import { azureOwnerAdapter } from "./resolveAzureOwner";
3
+ import type { EntraSnapshot } from "../inputTransferObject/entra/EntraSnapshot";
4
+ import type { AzureSnapshot } from "../../../core/azure/resources";
5
+
6
+ test("resolves owners from configurable tag names", () => {
7
+ const report = buildAzureOwnershipReport(resourceSnapshot(), entraSnapshot(), {
8
+ tags: {
9
+ businessOwner: {
10
+ confidence: "high"
11
+ },
12
+ technicalOwner: {
13
+ confidence: "medium"
14
+ }
15
+ },
16
+ ownerTargets: [
17
+ {
18
+ kind: "resourceGroup",
19
+ adapter: azureOwnerAdapter
20
+ }
21
+ ]
22
+ });
23
+
24
+ expect(report.owners).toEqual([
25
+ expect.objectContaining({
26
+ resourceGroup: "rg-payments",
27
+ owner: "sg-payments-platform",
28
+ confidence: "high",
29
+ source: "tag.businessOwner",
30
+ evidence: [{ user: "businessOwner=sg-payments-platform", date: null }]
31
+ }),
32
+ expect.objectContaining({
33
+ resourceGroup: "rg-billing",
34
+ owner: "alice@example.com",
35
+ confidence: "medium",
36
+ source: "tag.technicalOwner",
37
+ evidence: [{ user: "technicalOwner=Alice@Example.com", date: null }]
38
+ })
39
+ ]);
40
+ });
41
+
42
+ function resourceSnapshot(): AzureSnapshot {
43
+ return {
44
+ meta: {
45
+ provider: "azure",
46
+ snapshotVersion: "1",
47
+ createdAt: "2026-05-01T00:00:00.000Z",
48
+ activityDays: 30,
49
+ activityStartTime: "2026-04-01T00:00:00.000Z",
50
+ maxActivityRecords: 1000,
51
+ requestedSubscriptions: [],
52
+ subscriptionCount: 1,
53
+ resourceGroupCount: 2,
54
+ resourceCount: 0,
55
+ userAssignedManagedIdentityCount: 0,
56
+ activityLogCount: 0
57
+ },
58
+ subscriptions: [],
59
+ resourceGroups: [
60
+ {
61
+ subscriptionId: "sub-1",
62
+ subscriptionName: "Subscription Alpha",
63
+ resourceGroup: "rg-payments",
64
+ location: "westeurope",
65
+ tags: {
66
+ businessOwner: "sg-payments-platform"
67
+ }
68
+ },
69
+ {
70
+ subscriptionId: "sub-1",
71
+ subscriptionName: "Subscription Alpha",
72
+ resourceGroup: "rg-billing",
73
+ location: "westeurope",
74
+ tags: {
75
+ technicalOwner: "Alice@Example.com"
76
+ }
77
+ }
78
+ ],
79
+ resources: [],
80
+ userAssignedManagedIdentities: [],
81
+ roleAssignments: [],
82
+ activityLogs: []
83
+ };
84
+ }
85
+
86
+ function entraSnapshot(): EntraSnapshot {
87
+ return {
88
+ meta: {
89
+ provider: "entra",
90
+ snapshotVersion: "1",
91
+ createdAt: "2026-05-01T00:00:00.000Z",
92
+ tenantId: "tenant-1",
93
+ account: "admin@example.com",
94
+ scopes: [],
95
+ servicePrincipalCount: 0
96
+ },
97
+ servicePrincipals: []
98
+ };
99
+ }
@@ -0,0 +1,90 @@
1
+ import type { AzureSnapshot } from "../../../core/azure/resources";
2
+ import type { EntraSnapshot } from "../inputTransferObject/entra/EntraSnapshot";
3
+ import type { OwnerReport, OwnerReportRow } from "./azureOwnerReportTypes";
4
+ import { azureOwnershipConfig } from "./azureOwnershipConfig";
5
+ import { buildActivityIndex } from "./resolveAzureOwner";
6
+ import { buildServicePrincipalIndex } from "./azureActivityOwnershipEvidence";
7
+ import type { AzureReportConfig, AzureScopeOwnershipTarget } from "./azureOwnershipTypes";
8
+
9
+ export function buildAzureOwnershipReport(
10
+ resourceSnapshot: AzureSnapshot,
11
+ entraSnapshot: EntraSnapshot,
12
+ config: AzureReportConfig = azureOwnershipConfig
13
+ ): OwnerReport {
14
+ const context = {
15
+ resourceSnapshot,
16
+ entraSnapshot,
17
+ tags: config.tags,
18
+ activityLogIndex: buildActivityIndex(resourceSnapshot.activityLogs),
19
+ servicePrincipalIndex: buildServicePrincipalIndex(entraSnapshot.servicePrincipals)
20
+ };
21
+
22
+ const owners = config.ownerTargets.flatMap((targetConfig) => {
23
+ const targets = getTargets(targetConfig.kind, resourceSnapshot);
24
+ return targets.map((target): OwnerReportRow => {
25
+ const resolution = targetConfig.adapter.resolveOwner(target, context);
26
+ const identity = getTargetIdentity(target);
27
+
28
+ return {
29
+ ...identity,
30
+ ...resolution
31
+ };
32
+ });
33
+ });
34
+
35
+ return {
36
+ owners
37
+ };
38
+ }
39
+
40
+ function getTargets(
41
+ kind: AzureScopeOwnershipTarget["kind"],
42
+ resourceSnapshot: AzureSnapshot
43
+ ): AzureScopeOwnershipTarget[] {
44
+ if (kind === "subscription") {
45
+ return resourceSnapshot.subscriptions.map((subscription) => ({
46
+ kind,
47
+ subscription
48
+ }));
49
+ }
50
+
51
+ return resourceSnapshot.resourceGroups.map((resourceGroup) => ({
52
+ kind,
53
+ resourceGroup
54
+ }));
55
+ }
56
+
57
+ function getTargetIdentity(target: AzureScopeOwnershipTarget): Pick<
58
+ OwnerReportRow,
59
+ "targetKey" | "kind" | "subscriptionId" | "subscriptionName" | "resourceGroup"
60
+ > {
61
+ if (target.kind === "subscription") {
62
+ return {
63
+ targetKey: getAzureOwnerTargetKey(target.kind, target.subscription.subscriptionId, null),
64
+ kind: target.kind,
65
+ subscriptionId: target.subscription.subscriptionId,
66
+ subscriptionName: target.subscription.subscriptionName,
67
+ resourceGroup: null
68
+ };
69
+ }
70
+
71
+ return {
72
+ targetKey: getAzureOwnerTargetKey(
73
+ target.kind,
74
+ target.resourceGroup.subscriptionId,
75
+ target.resourceGroup.resourceGroup
76
+ ),
77
+ kind: target.kind,
78
+ subscriptionId: target.resourceGroup.subscriptionId,
79
+ subscriptionName: target.resourceGroup.subscriptionName,
80
+ resourceGroup: target.resourceGroup.resourceGroup
81
+ };
82
+ }
83
+
84
+ function getAzureOwnerTargetKey(
85
+ kind: OwnerReportRow["kind"],
86
+ subscriptionId: string,
87
+ resourceGroup: string | null
88
+ ): string {
89
+ return [kind, subscriptionId.toLowerCase(), (resourceGroup ?? "").toLowerCase()].join(":");
90
+ }
@@ -0,0 +1,87 @@
1
+ import { buildZeroTrustAssessmentAuditFindingTarget } from "../../../core/ownership/OwnershipTarget";
2
+ import type { EntraServicePrincipal } from "../inputTransferObject/entra/EntraServicePrincipal";
3
+ import type { AzureUserAssignedManagedIdentity } from "../../../core/azure/resources";
4
+ import {
5
+ buildAzureManagedIdentityOwnershipTargets,
6
+ buildEntraServicePrincipalOwnershipTargets
7
+ } from "./buildAzureOwnershipTargets";
8
+
9
+ test("maps Azure managed identities to generic ownership targets", () => {
10
+ const identity: AzureUserAssignedManagedIdentity = {
11
+ subscriptionId: "sub-1",
12
+ subscriptionName: "Production",
13
+ resourceId: "/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-1",
14
+ name: "id-1",
15
+ resourceGroup: "rg-1",
16
+ location: "westeurope",
17
+ clientId: "client-1",
18
+ principalId: "principal-1",
19
+ tenantId: "tenant-1",
20
+ tags: null
21
+ };
22
+
23
+ expect(buildAzureManagedIdentityOwnershipTargets([identity])).toEqual([
24
+ {
25
+ id: identity.resourceId,
26
+ kind: "azure.managedIdentity",
27
+ displayName: "id-1",
28
+ sourceProvider: "azure",
29
+ technicalId: "principal-1",
30
+ refs: [
31
+ { type: "azure.subscription", id: "sub-1", label: "Production" },
32
+ { type: "azure.resourceGroup", id: "rg-1" },
33
+ { type: "entra.servicePrincipal", id: "principal-1" },
34
+ { type: "entra.application", id: "client-1" },
35
+ { type: "entra.tenant", id: "tenant-1" }
36
+ ]
37
+ }
38
+ ]);
39
+ });
40
+
41
+ test("maps Entra service principals to generic ownership targets", () => {
42
+ const servicePrincipal: EntraServicePrincipal = {
43
+ id: "sp-1",
44
+ appId: "app-1",
45
+ displayName: "Payroll API",
46
+ appDisplayName: "Payroll",
47
+ servicePrincipalType: "Application",
48
+ publisherName: null,
49
+ accountEnabled: true,
50
+ appOwnerOrganizationId: "tenant-1",
51
+ homepage: null,
52
+ loginUrl: null,
53
+ replyUrls: [],
54
+ servicePrincipalNames: [],
55
+ tags: []
56
+ };
57
+
58
+ expect(buildEntraServicePrincipalOwnershipTargets([servicePrincipal])).toEqual([
59
+ {
60
+ id: "sp-1",
61
+ kind: "entra.servicePrincipal",
62
+ displayName: "Payroll API",
63
+ sourceProvider: "entra",
64
+ technicalId: "app-1",
65
+ refs: [
66
+ { type: "entra.application", id: "app-1", label: "Payroll" },
67
+ { type: "entra.tenant", id: "tenant-1" }
68
+ ]
69
+ }
70
+ ]);
71
+ });
72
+
73
+ test("creates a Zero Trust Assessment audit finding ownership target placeholder", () => {
74
+ expect(
75
+ buildZeroTrustAssessmentAuditFindingTarget({
76
+ id: "finding-1",
77
+ displayName: "Missing accountable owner",
78
+ riskLevel: "high"
79
+ })
80
+ ).toEqual({
81
+ id: "finding-1",
82
+ kind: "zta.auditFinding",
83
+ displayName: "Missing accountable owner",
84
+ sourceProvider: "zeroTrustAssessment",
85
+ riskLevel: "high"
86
+ });
87
+ });