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.
- package/LICENSE +183 -0
- package/README.md +209 -0
- package/bin/ownerlens.js +92 -0
- package/dist/assets/index-B9aAYpVl.css +1 -0
- package/dist/assets/index-BcwLk2bx.js +10 -0
- package/dist/index.html +13 -0
- package/package.json +73 -0
- package/src/App.tsx +18 -0
- package/src/components/azure/AzureComponent.test.tsx +625 -0
- package/src/components/azure/AzureComponent.tsx +189 -0
- package/src/components/azure/AzureRbacComponent.tsx +104 -0
- package/src/components/azure/ClosableAzureTab.tsx +42 -0
- package/src/components/azure/EntraPermissionsComponent.tsx +194 -0
- package/src/components/azure/ManagedIdentityComponent.test.tsx +324 -0
- package/src/components/azure/ManagedIdentityComponent.tsx +141 -0
- package/src/components/azure/ResourceGroupComponent.tsx +157 -0
- package/src/components/azure/ServicePrincipalComponent.test.tsx +457 -0
- package/src/components/azure/ServicePrincipalComponent.tsx +155 -0
- package/src/components/azure/ServicePrincipalFieldRenderers.tsx +140 -0
- package/src/components/azure/ZtaComponent.test.tsx +267 -0
- package/src/components/azure/ZtaComponent.tsx +276 -0
- package/src/components/azure/ZtaRemediationBadge.tsx +70 -0
- package/src/components/azure/api.ts +216 -0
- package/src/components/azure/azureReportConfig.ts +247 -0
- package/src/core/azure/azureRbac.ts +70 -0
- package/src/core/azure/entra/index.ts +1 -0
- package/src/core/azure/entra/managedIdentity.ts +21 -0
- package/src/core/azure/entra/servicePrincipal.ts +34 -0
- package/src/core/azure/entra/types.ts +56 -0
- package/src/core/azure/identityEnrichment.ts +65 -0
- package/src/core/azure/resources.ts +141 -0
- package/src/core/azure/ztaReport.ts +58 -0
- package/src/core/config.ts +39 -0
- package/src/core/ownership/OwnershipTarget.ts +32 -0
- package/src/core/ownership/resolveOwner.ts +5 -0
- package/src/core/ownership/types.ts +14 -0
- package/src/core/risk/types.ts +1 -0
- package/src/core/runtime/index.ts +1 -0
- package/src/core/runtime/localSnapshotFiles.ts +74 -0
- package/src/core/runtime/rest.ts +61 -0
- package/src/lib/searchFilterUtils.ts +17 -0
- package/src/lib/utils.ts +48 -0
- package/src/main.tsx +10 -0
- package/src/providers/azure/identities/azureIdentityTypes.ts +1 -0
- package/src/providers/azure/identities/buildAzureManagedIdentityAssignmentIndex.test.ts +32 -0
- package/src/providers/azure/identities/buildAzureManagedIdentityAssignmentIndex.ts +35 -0
- package/src/providers/azure/identities/userAssignedIdentityAssignments.ts +52 -0
- package/src/providers/azure/inputTransferObject/entra/EntraAppRoleAssignment.ts +10 -0
- package/src/providers/azure/inputTransferObject/entra/EntraApplication.ts +27 -0
- package/src/providers/azure/inputTransferObject/entra/EntraOAuth2PermissionGrant.ts +8 -0
- package/src/providers/azure/inputTransferObject/entra/EntraServicePrincipal.ts +43 -0
- package/src/providers/azure/inputTransferObject/entra/EntraSnapshot.ts +13 -0
- package/src/providers/azure/inputTransferObject/entra/EntraSnapshotMeta.ts +12 -0
- package/src/providers/azure/inputTransferObject/resources/AzureActivityLog.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureResource.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureResourceGroup.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureRoleAssignment.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureSnapshot.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureSnapshotMeta.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureSubscription.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureUserAssignedManagedIdentity.ts +1 -0
- package/src/providers/azure/ownership/azureActivityOwnershipEvidence.ts +60 -0
- package/src/providers/azure/ownership/azureOwnerReportTypes.ts +13 -0
- package/src/providers/azure/ownership/azureOwnershipConfig.ts +21 -0
- package/src/providers/azure/ownership/azureOwnershipTypes.ts +46 -0
- package/src/providers/azure/ownership/buildAzureOwnershipReport.test.ts +99 -0
- package/src/providers/azure/ownership/buildAzureOwnershipReport.ts +90 -0
- package/src/providers/azure/ownership/buildAzureOwnershipTargets.test.ts +87 -0
- package/src/providers/azure/ownership/buildAzureOwnershipTargets.ts +42 -0
- package/src/providers/azure/ownership/resolveAzureOwner.ts +146 -0
- package/src/providers/azure/runtime/DisabledEvidenceStore.ts +34 -0
- package/src/providers/azure/runtime/EnrichmentService.ts +35 -0
- package/src/providers/azure/runtime/LocalReportRuntime.test.ts +2318 -0
- package/src/providers/azure/runtime/LocalReportRuntime.ts +302 -0
- package/src/providers/azure/runtime/RuntimeHost.ts +60 -0
- package/src/providers/azure/runtime/SnapshotImporter.ts +44 -0
- package/src/providers/azure/runtime/enrichment/azureIdentityEnrichment.ts +523 -0
- package/src/providers/azure/runtime/enrichment/azureScopeClassifier.ts +30 -0
- package/src/providers/azure/runtime/enrichment/evaluateAzureRoleAssignmentRisk.ts +88 -0
- package/src/providers/azure/runtime/entra/EntraCollectionQueryService.ts +307 -0
- package/src/providers/azure/runtime/entra/LocalEntraReportRuntime.ts +227 -0
- package/src/providers/azure/runtime/entra/appRoleAssignmentsTable.ts +52 -0
- package/src/providers/azure/runtime/entra/applicationsTable.ts +175 -0
- package/src/providers/azure/runtime/entra/entraServicePrincipalMapper.ts +63 -0
- package/src/providers/azure/runtime/entra/localReportRuntimeRest.ts +41 -0
- package/src/providers/azure/runtime/entra/oauth2PermissionGrantsTable.ts +48 -0
- package/src/providers/azure/runtime/entra/principalProjection.ts +173 -0
- package/src/providers/azure/runtime/entra/servicePrincipalsTable.ts +149 -0
- package/src/providers/azure/runtime/entra/snapshotMetadataTable.ts +18 -0
- package/src/providers/azure/runtime/entra/snapshotStore.ts +102 -0
- package/src/providers/azure/runtime/localReportCollections.ts +101 -0
- package/src/providers/azure/runtime/localReportRuntimeRest.ts +71 -0
- package/src/providers/azure/runtime/resources/AzureResourcesCollectionQueryService.ts +145 -0
- package/src/providers/azure/runtime/resources/LocalAzureResourcesReportRuntime.ts +114 -0
- package/src/providers/azure/runtime/resources/disabledOwnerEvidenceTable.ts +60 -0
- package/src/providers/azure/runtime/resources/localReportRuntimeRest.ts +81 -0
- package/src/providers/azure/runtime/resources/resourceGroupOwnership.ts +90 -0
- package/src/providers/azure/runtime/resources/snapshotMetadataTable.ts +19 -0
- package/src/providers/azure/runtime/resources/snapshotStore.ts +128 -0
- package/src/providers/azure/runtime/resources/tables.ts +441 -0
- package/src/providers/azure/runtime/runtimeRestQuery.ts +46 -0
- package/src/providers/azure/runtime/runtimeSqlSchema.ts +357 -0
- package/src/providers/azure/runtime/zta/Discovery.ts +141 -0
- package/src/providers/azure/runtime/zta/LocalZeroTrustAssessmentReportRuntime.ts +86 -0
- package/src/providers/azure/runtime/zta/ZeroTrustAssessmentQueryService.ts +124 -0
- package/src/providers/azure/runtime/zta/localReportRuntimeRest.ts +15 -0
- package/src/providers/azure/runtime/zta/snapshotMetadataTable.ts +77 -0
- package/src/providers/azure/runtime/zta/snapshotStore.ts +112 -0
- package/src/providers/azure/runtime/zta/tables.ts +361 -0
- package/src/providers/azure/runtime/zta/types.ts +7 -0
- package/src/providers/azure/runtime/zta/ztaReportMapper.ts +12 -0
- package/src/report/applyCollectionControls.ts +289 -0
- package/src/report/buildCollectionColumns.tsx +38 -0
- package/src/report/components/ConfidenceBadge.tsx +10 -0
- package/src/report/components/EvidenceList.test.ts +25 -0
- package/src/report/components/EvidenceList.tsx +52 -0
- package/src/report/components/GenericTable.tsx +373 -0
- package/src/report/components/PermissionRiskBadge.tsx +19 -0
- package/src/report/components/reportTableControls.test.ts +175 -0
- package/src/report/components/reportTableControls.tsx +483 -0
- package/src/report/components/ui/badge.tsx +35 -0
- package/src/report/components/ui/button.tsx +38 -0
- package/src/report/components/ui/card.tsx +23 -0
- package/src/report/components/ui/input.tsx +15 -0
- package/src/report/components/ui/table.tsx +44 -0
- package/src/report/components/ui/tabs.tsx +29 -0
- package/src/report/export/csv.ts +34 -0
- package/src/report/ownerManualPrecheck.test.ts +137 -0
- package/src/report/ownerManualPrecheck.ts +132 -0
- package/src/report/reportArchitecture.test.ts +125 -0
- package/src/report/reportTypes.ts +54 -0
- package/src/report/reportValueRenderers.tsx +54 -0
- package/src/report/runtimeCollectionQuery.ts +23 -0
- package/src/report/types.ts +14 -0
- package/src/styles.css +43 -0
- package/tools/README.md +108 -0
- package/tools/azure-activity-check.ps1 +164 -0
- package/tools/collect-azure.ps1 +54 -0
- package/tools/collect-entra.ps1 +47 -0
- package/tools/collect-scripts.test.ts +22 -0
- package/tools/prepare-entra-snapshot.ps1 +403 -0
- package/tools/prepare-entra-snapshot.test.ts +14 -0
- package/tools/prepare-resource-snapshot.ps1 +345 -0
- 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,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
|
+
});
|