Skip to content
Merged
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
197 changes: 197 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# SMAD-X — Roadmap: Domain Evolution & AD Delegations

> Last updated: 2025-06-01

---

## Part 1 — Domain Timeline / Evolution

### Step 1 — Model `DomainSnapshot`
**File:** `SMAD-X\Models\DomainSnapshot.cs`
- `FilePath` (string) — path to the `.smad-x.json` source file
- `DomainName` (string) — root domain name
- `SnapshotDate` (DateTime) — extracted from the `ModifiedDate` field of the root object
- `Root` (ADObject) — fully deserialized tree

---

### Step 2 — Diff Model `ADChangeItem`
**File:** `SMAD-X\Models\ADChangeItem.cs`
- `ChangeType` : enum `Added | Removed | Modified`
- `ObjectName` (string)
- `DistinguishedName` (string) — stable key for comparison
- `ObjectType` (ADObjectType)
- `ChangedFields` : `List<(string Field, string OldValue, string NewValue)>`
- Tracked fields: `Tier`, `Description`, `MemberOf`, `LinkedGPOs`

---

### Step 3 — Service `ADDiffService`
**File:** `SMAD-X\Services\ADDiffService.cs`

Main method:
```csharp
public List<ADChangeItem> Compare(ADObject before, ADObject after)
```
- Recursive diff indexed by `DistinguishedName`
- Detects: **additions**, **removals**, **field modifications**

---

### Step 4 — `DomainTimelineViewModel`
**File:** `SMAD-X\ViewModels\DomainTimelineViewModel.cs`
- `ObservableCollection<DomainSnapshot> Snapshots`
- `DomainSnapshot? SnapshotBefore` / `SnapshotAfter` (the two snapshots to compare)
- `ObservableCollection<ADChangeItem> Changes` (diff result)
- `FilterType` : All / Added / Removed / Modified
- `FilterText` (string)

Commands:
- `AddSnapshotCommand` — `OpenFilePicker` multi-select `*.smad-x.json`
- `RemoveSnapshotCommand`
- `CompareCommand` — calls `ADDiffService.Compare`

---

### Step 5 — View `DomainTimelineWindow`
**Files:** `SMAD-X\Views\DomainTimelineWindow.axaml` / `.axaml.cs`

Layout:
- **Left panel**: list of loaded snapshots (date + domain name), Add / Remove buttons
- **Selectors**: "Before" / "After" ComboBoxes fed by `Snapshots`
- **Right panel**: `DataGrid` with columns:
- Change type (colored icon: green = Added, red = Removed, orange = Modified)
- Object, DN, AD Type, Modified fields

---

### Step 6 — Main menu integration
**Files:** `MainWindow.axaml` + `MainWindowViewModel.cs`
- Menu: `Analyse > Domain Evolution` — shortcut `Ctrl+T`
- Command: `ShowTimelineCommand`

---

## Part 2 — Active Directory Delegations

Delegations in Active Directory express **who** (trustee: user or group) can perform **what action** (right)
on **which scope** (OU or Container). This includes:

- **Password reset**: a group (e.g. `Helpdesk_L1`) can reset passwords for all user accounts in a given OU.
- **Computer account creation**: a group can create/delete computer objects in a specific OU.
- **Account unlock**: a group can unlock user accounts.
- **Attribute write**: a group can modify specific attributes (e.g. `telephoneNumber`, `member`).
- **Generic control**: full control over objects in an OU (e.g. `GenericAll`, `GenericWrite`).

These delegations are stored in the ACL (`Get-Acl`) of each OU/Container and must be captured
during the PowerShell AD analysis, then visualized in a dedicated view.

---

### Step 7 — Model `ADDelegation`
**File:** `SMAD-X\Models\ADDelegation.cs`
- `TrusteeName` (string) — SAMAccountName of the trustee (user or group)
- `TrusteeType` : enum `User | Group | Computer`
- `TargetDN` (string) — DN of the OU/Container where the delegation is defined
- `Right` (string) — e.g. `ResetPassword`, `CreateChild:computer`, `GenericAll`, `WriteProperty:member`
- `RightCategory` : enum `PasswordReset | ComputerManagement | AccountUnlock | AttributeWrite | FullControl | Other`
- `IsInherited` (bool) — whether the ACE is inherited from a parent OU
- `Tier` (string?) — inherited from the target object context

---

### Step 8 — Add `Delegations` to `ADObject`
**File:** `SMAD-X\Models\ADObject.cs`

Add the property:
```csharp
public ObservableCollection<ADDelegation> Delegations { get; set; } = new();
```

---

### Step 9 — PowerShell export of delegations
**File:** `SMAD-X\Services\ADImportPowerShellService.cs`

In the `Build-NodeJson` function of the generated script:
- For OUs and Containers, call `Get-Acl` on the AD object
- Extract `ActiveDirectoryAccessRule` entries (non-inherited by default, with an option for inherited)
- Filter out default/system ACEs (e.g. `NT AUTHORITY\SYSTEM`, `BUILTIN\Administrators`)
- Categorize each ACE into a `RightCategory`:
- `ResetPassword` extended right → `PasswordReset`
- `CreateChild` for computer objects → `ComputerManagement`
- `WriteProperty:lockoutTime` → `AccountUnlock`
- `WriteProperty:<attribute>` → `AttributeWrite`
- `GenericAll` / `GenericWrite` → `FullControl`
- Serialize under the `"Delegations"` key in the JSON output

Expected JSON format per delegation:
```json
{
"TrusteeName": "Helpdesk_L1",
"TrusteeType": "Group",
"TargetDN": "OU=Users,DC=contoso,DC=com",
"Right": "ResetPassword",
"RightCategory": "PasswordReset",
"IsInherited": false,
"Tier": "Tier 1"
}
```

---

### Step 10 — `DelegationsViewModel`
**File:** `SMAD-X\ViewModels\DelegationsViewModel.cs`

- Recursively walks the entire `ADObject` tree and flattens all `Delegations` from every node
- `ObservableCollection<ADDelegation> AllDelegations`
- `ObservableCollection<ADDelegation> FilteredDelegations`

Filters:
- `FilterTrustee` (string) — filter by trustee SAMAccountName
- `FilterRight` (string) — filter by right/category
- `FilterTier` (string) — filter by Tier
- `FilterInherited` (bool?) — include / exclude inherited ACEs
- `FilterTargetOU` (string) — filter by target OU DN

Commands:
- `ApplyFiltersCommand`
- `ExportToCsvCommand` — exports `FilteredDelegations` to CSV

---

### Step 11 — View `DelegationsWindow`
**Files:** `SMAD-X\Views\DelegationsWindow.axaml` / `.axaml.cs`

Layout:
- **Filter bar** at the top: Trustee, Right/Category, Tier, Target OU, "Include inherited" checkbox
- **DataGrid** with columns: Trustee, Type, Right, Category, Target OU, Inherited yes/no, Tier
- **Export CSV** button

---

### Step 12 — Main menu integration
**Files:** `MainWindow.axaml` + `MainWindowViewModel.cs`
- Menu: `Analyse > Delegations` — shortcut `Ctrl+D`
- Command: `ShowDelegationsCommand`

---

## Implementation order

| # | Step | Dependencies |
|---|------|--------------|
| 1 | Model DomainSnapshot | — |
| 2 | Model ADChangeItem | — |
| 3 | Service ADDiffService | Steps 1, 2 |
| 4 | DomainTimelineViewModel | Step 3 |
| 5 | View DomainTimelineWindow | Step 4 |
| 6 | Menu integration (Timeline) | Step 5 |
| 7 | Model ADDelegation | — |
| 8 | Delegations property in ADObject | Step 7 |
| 9 | PowerShell export of delegations | Step 8 |
| 10 | DelegationsViewModel | Steps 7, 8 |
| 11 | View DelegationsWindow | Step 10 |
| 12 | Menu integration (Delegations) | Step 11 |

> **Note:** Part 1 (Timeline) and Part 2 (Delegations) are independent and can be developed in parallel.
1 change: 1 addition & 0 deletions SMAD-X/App.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
<StyleInclude Source="avares://SMAD-X/Styles/AppStyles.axaml"/>
</Application.Styles>
</Application>
22 changes: 22 additions & 0 deletions SMAD-X/Graph/GraphBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,28 @@ o.Type is ADObjectType.User or ADObjectType.Computer or ADObjectType.GMSA
}
}

// ── Delegations ACL : trustee → OU/Container cible ──
if (filter.ShowDelegations)
{
foreach (var obj in allObjects.Where(o => o.Delegations.Count > 0))
{
if (!_nodeMap.TryGetValue(obj.Name, out var targetNode)) continue;

foreach (var del in obj.Delegations)
{
if (!_nodeMap.TryGetValue(del.TrusteeName, out var trusteeNode)) continue;

bool alreadyEdge = Edges.Any(e =>
e.Type == EdgeType.Delegation &&
e.Source == trusteeNode &&
e.Target == targetNode &&
e.GpoName == del.Right);
if (!alreadyEdge)
Edges.Add(new GraphEdge(trusteeNode, targetNode, EdgeType.Delegation, del.Right));
}
}
}

// Supprimer les nœuds isolés si le filtre le demande
if (!filter.ShowIsolated)
{
Expand Down
2 changes: 2 additions & 0 deletions SMAD-X/Graph/GraphCanvas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class GraphCanvas : Control
{ EdgeType.GpoInheritance, Color.FromRgb(0xD0, 0x80, 0x00) },
{ EdgeType.PsoSubject, Color.FromRgb(0xC5, 0x07, 0x1F) },
{ EdgeType.ParentChild, Color.FromRgb(0x60, 0x60, 0x60) },
{ EdgeType.Delegation, Color.FromRgb(0xFF, 0xB3, 0x00) }, // amber
};

private static readonly Dictionary<ADObjectType, string> NodeIcons = new()
Expand Down Expand Up @@ -403,6 +404,7 @@ private void DrawLegend(DrawingContext ctx)
(EdgeColors[EdgeType.MemberOf], false, "MemberOf"),
(EdgeColors[EdgeType.GroupNesting], true, "Group in Group"),
(EdgeColors[EdgeType.PsoSubject], false, "PSO Subject"),
(EdgeColors[EdgeType.Delegation], false, "Délégation"),
};

const double lx = 10;
Expand Down
4 changes: 3 additions & 1 deletion SMAD-X/Graph/GraphEdge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ public enum EdgeType
GpoLink,
GpoInheritance, // GPO héritée d'un parent (Domain → OU ou OU parente → OU enfant)
PsoSubject,
ParentChild
ParentChild,
Delegation // Délégation ACL : trustee → OU/Container cible
}

/// <summary>
Expand All @@ -30,6 +31,7 @@ public class GraphEdge
EdgeType.GpoInheritance => $"⬇ Héritage GPO",
EdgeType.PsoSubject => "PSO Subject",
EdgeType.ParentChild => "Contains",
EdgeType.Delegation => GpoName ?? "Delegation",
_ => string.Empty
};

Expand Down
1 change: 1 addition & 0 deletions SMAD-X/Graph/GraphFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class GraphFilter
public bool ShowGpoLinks { get; set; } = true;
public bool ShowGpoInheritance { get; set; } = true;
public bool ShowPsoLinks { get; set; } = true;
public bool ShowDelegations { get; set; } = true;
public bool ShowIsolated { get; set; } = false;
}
}
1 change: 1 addition & 0 deletions SMAD-X/Helpers/LocalizationProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ public LocalizationProxy()
public string GraphFilterGpoLinks => _localizationService["Graph.FilterGpoLinks"];
public string GraphFilterGpoInheritance => _localizationService["Graph.FilterGpoInheritance"];
public string GraphFilterPso => _localizationService["Graph.FilterPso"];
public string GraphFilterDelegations => _localizationService["Graph.FilterDelegations"];
public string GraphFilterHierarchy => _localizationService["Graph.FilterHierarchy"];
public string GraphFilterIsolated => _localizationService["Graph.FilterIsolated"];
public string GraphFitViewTooltip => _localizationService["Graph.FitViewTooltip"];
Expand Down
48 changes: 48 additions & 0 deletions SMAD-X/Models/ADChangeItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Collections.Generic;

namespace SMADX.Models
{
public enum ChangeType
{
Added,
Removed,
Modified
}

/// <summary>
/// Broad category for the kind of change — used as a filter axis in the Timeline UI.
/// </summary>
public enum ChangeCategory
{
Structure, // object added / removed / renamed / moved
MemberOf, // group membership changes
GPO, // GPO link added / removed
PSO, // PSO assignment or settings changed
Delegation // delegation (ACE) added / removed / changed
}

/// <summary>
/// Represents a single change detected between two AD snapshots.
/// </summary>
public class ADChangeItem
{
public ChangeType ChangeType { get; set; }
public ChangeCategory ChangeCategory { get; set; } = ChangeCategory.Structure;
public string ObjectName { get; set; } = string.Empty;
public string DistinguishedName { get; set; } = string.Empty;
public ADObjectType ObjectType { get; set; }

/// <summary>
/// For Modified items: list of (Field, OldValue, NewValue) tuples.
/// </summary>
public List<(string Field, string OldValue, string NewValue)> ChangedFields { get; set; } = new();

public string ChangedFieldsSummary =>
ChangedFields.Count == 0
? string.Empty
: string.Join("; ", ChangedFields.ConvertAll(f =>
string.IsNullOrEmpty(f.OldValue) ? $"+{f.Field}: {f.NewValue}"
: string.IsNullOrEmpty(f.NewValue) ? $"-{f.Field}: {f.OldValue}"
: $"{f.Field}: {f.OldValue} → {f.NewValue}"));
}
}
44 changes: 44 additions & 0 deletions SMAD-X/Models/ADDelegation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace SMADX.Models
{
public enum TrusteeType
{
User,
Group,
Computer
}

public enum RightCategory
{
PasswordReset,
ComputerManagement,
AccountUnlock,
AttributeWrite,
FullControl,
Other
}

/// <summary>
/// Represents an AD delegation (ACE) found on an OU or Container.
/// </summary>
public class ADDelegation
{
/// <summary>SAMAccountName of the trustee (user or group).</summary>
public string TrusteeName { get; set; } = string.Empty;

public TrusteeType TrusteeType { get; set; }

/// <summary>Distinguished name of the OU/Container on which the ACE is set.</summary>
public string TargetDN { get; set; } = string.Empty;

/// <summary>Human-readable right, e.g. ResetPassword, CreateChild:computer.</summary>
public string Right { get; set; } = string.Empty;

public RightCategory RightCategory { get; set; }

/// <summary>True when the ACE is inherited from a parent OU.</summary>
public bool IsInherited { get; set; }

/// <summary>Tier context of the target object, if known.</summary>
public string? Tier { get; set; }
}
}
5 changes: 5 additions & 0 deletions SMAD-X/Models/ADObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ public string TierColor
/// </summary>
public ObservableCollection<string> PSOAppliesTo { get; set; } = new();

/// <summary>
/// ACL-based delegations found on this OU/Container.
/// </summary>
public ObservableCollection<ADDelegation> Delegations { get; set; } = new();

// --- Propriétés spécifiques PSO (Fine-Grained Password Policy) ---

public int? PSOPrecedence { get; set; }
Expand Down
Loading