diff --git a/SharepointToolbox/Localization/Strings.fr.resx b/SharepointToolbox/Localization/Strings.fr.resx
index ccd1a19..0a6f348 100644
--- a/SharepointToolbox/Localization/Strings.fr.resx
+++ b/SharepointToolbox/Localization/Strings.fr.resx
@@ -583,6 +583,8 @@ Cette action est irréversible.
Options d'exportationFusionner les permissions en doubleMasquer les noms bruts (SharingLinks, Limited Access)
+ Exclure les liens de partage
+ Exclure les groupes système (Limited Access)Enregistrer l'appSupprimer l'app
@@ -687,6 +689,7 @@ Cette action est irréversible.
GénéréGénéré :membres indisponibles
+ Groupe videLien(sans ext.)(sans extension)
@@ -784,4 +787,84 @@ Cette action est irréversible.
utilisateur(s)fichierssites
+ entrées
+
+ Mode simplifié
+ Regroupe les permissions brutes SharePoint en libellés lisibles (Propriétaire, Éditeur, Contributeur, Lecteur, Lecture seule) et colore les lignes par niveau de risque. Utile pour un aperçu rapide de la sécurité sans jargon technique.
+ Fusionner les permissions
+ Lorsqu'activé, les entrées de permission multiples pour le même utilisateur ou groupe sont regroupées en une seule ligne dans l'export, réduisant la taille du rapport. Désactivez pour voir chaque permission individuellement.
+ Masquer les groupes système
+ Supprime les groupes système créés automatiquement par SharePoint (ex. « Excel Services Viewers », groupes « SharingLinks.* »). Ces groupes sont gérés en interne par SharePoint et ne sont généralement pas pertinents pour les audits d'accès.
+ Exclure les liens de partage
+ Supprime les entrées de lien de partage des résultats et des exports (ex. « Tout le monde avec le lien », liens à l'échelle de l'organisation). Utile pour ne conserver que les permissions directes des utilisateurs et groupes.
+ Exclure les groupes système (Limited Access)
+ Supprime les entrées « Limited Access System Group For Web/List » des résultats et des exports. SharePoint crée ces groupes automatiquement lorsqu'un utilisateur a accès à un élément spécifique ; ils sont rarement pertinents pour les audits d'accès.
+ Inclure les permissions héritées
+ Par défaut, seuls les objets avec des permissions uniques (rompues) sont affichés. Activez pour inclure les objets qui héritent les permissions d'un parent et obtenir une vue complète des accès.
+ Mode de fractionnement de l'export
+ Fichier unique : tous les résultats dans un seul fichier CSV ou HTML.
+
+Fractionner par site : crée un fichier séparé pour chaque collection de sites. Utile pour les grandes tenances multi-sites.
+ Recherche de fichiers KQL
+ Recherche des fichiers dans vos sites SharePoint via KQL (Keyword Query Language). Le champ mot-clé est optionnel — laissez-le vide pour retourner tous les fichiers correspondant aux filtres actifs. Combinez les filtres de date, auteur et bibliothèque pour affiner les résultats.
+ Filtre regex sur le nom de fichier
+ Filtre les résultats côté client avec une expression régulière .NET appliquée aux noms de fichiers. Exemple : \.pdf$ correspond uniquement aux PDF. Laissez vide pour ignorer ce filtre. L'expression est insensible à la casse.
+ Politique de nettoyage des versions
+ Supprime définitivement les anciennes versions de documents des bibliothèques SharePoint. Seules les N versions les plus récentes sont conservées — les versions plus anciennes sont supprimées de façon permanente et ne peuvent pas être récupérées. Effectuez d'abord une analyse pour prévisualiser les suppressions.
+ Conserver la première version
+ Conserve toujours la version 1.0 (originale) de chaque document, indépendamment du paramètre « Conserver les N dernières ». Utile pour maintenir une trace de l'état initial du document.
+ Confirmer avant suppression
+ Lorsqu'activé, une boîte de dialogue de confirmation apparaît pour chaque fichier avant la suppression des versions. Décochez pour un traitement en lot sans intervention.
+ Critères de détection des doublons
+ Deux éléments sont identifiés comme doublons quand leurs noms correspondent ET que tous les critères supplémentaires cochés correspondent également. Plus de critères cochés = moins de groupes, mais plus précis. Nom uniquement : trouve les fichiers avec le même nom, quel que soit leur contenu.
+ Inclure le dossier source
+ Lorsqu'activé, le dossier source lui-même est recréé à la destination (ex. transférer « Rapports » crée un dossier « Rapports/ » à la cible). Lorsque désactivé, seul le contenu du dossier est transféré — utile pour fusionner du contenu dans un dossier existant.
+ Copier uniquement le contenu
+ Lorsqu'activé, seuls les fichiers et sous-dossiers à l'intérieur du dossier sélectionné sont transférés — le dossier lui-même n'est pas recréé à la destination.
+ Politique de conflit de fichiers
+ Définit ce qui se passe quand un fichier du même nom existe déjà à la destination :
+
+• Ignorer — laisser le fichier destination inchangé.
+• Écraser — remplacer le fichier destination par le fichier source.
+• Renommer — conserver les deux en ajoutant un suffixe numérique au fichier transféré.
+ Ajout de membres en masse — Format CSV
+ Le fichier CSV doit contenir ces colonnes (en-têtes obligatoires, ordre libre) :
+• GroupName — le nom exact du groupe SharePoint
+• Email — l'adresse e-mail de l'utilisateur
+• Role — Member, Owner ou Visitor
+
+Cliquez sur « Charger l'exemple » pour ouvrir un fichier d'exemple pré-rempli.
+ Création de sites en masse — Format CSV
+ Le fichier CSV doit contenir ces colonnes :
+• Name — le nom d'affichage du nouveau site
+• Alias — alias d'URL (sans espaces ; fait partie de l'URL du site)
+• Type — TeamSite ou CommunicationSite
+• Owners — liste d'adresses e-mail des propriétaires séparées par des virgules
+
+Cliquez sur « Charger l'exemple » pour ouvrir un fichier d'exemple pré-rempli.
+ Créer une structure de dossiers — Format CSV
+ Crée une hiérarchie de dossiers dans une bibliothèque SharePoint à partir d'un fichier CSV. Chaque ligne définit un chemin avec jusqu'à 4 niveaux (Level1–Level4). Laissez les colonnes des niveaux inférieurs vides pour des chemins plus courts.
+
+Exemple : Contrats | 2024 | T1 | (vide)
+Crée : Bibliothèque / Contrats / 2024 / T1
+ Capturer un modèle de site
+ Enregistre la structure du site sélectionné (bibliothèques, dossiers, permissions, paramètres et logo) comme modèle réutilisable stocké localement. Le site source n'est pas modifié.
+
+Sélectionnez les éléments à capturer avec les cases à cocher ci-dessus.
+ Appliquer le modèle à un nouveau site
+ Crée un nouveau site SharePoint et reproduit la structure du modèle sélectionné — bibliothèques, dossiers, permissions, paramètres et logo. Le modèle source et le site d'origine ne sont pas affectés.
+
+Fournissez un nom d'affichage et un alias d'URL avant de cliquer sur Appliquer.
+ Mode Recherche vs Mode Navigation
+ Mode Recherche : tapez un nom ou e-mail pour trouver un utilisateur via Azure AD. Les résultats apparaissent dans une liste — cliquez pour sélectionner.
+
+Mode Navigation : charge tous les utilisateurs du répertoire de la tenant. Utilisez le filtre pour trouver un utilisateur, puis double-cliquez pour l'ajouter à l'audit.
+ Audit d'accès vs Audit des permissions
+ L'onglet Permissions analyse les objets (bibliothèques, dossiers, éléments) pour montrer qui y a accès.
+
+Cet onglet fait l'inverse : vous sélectionnez un ou plusieurs utilisateurs et il trouve chaque objet auquel ils peuvent accéder — y compris via des groupes SharePoint ou Active Directory.
+ Bibliothèques masquées
+ Analyse les bibliothèques SharePoint cachées dans la navigation normale du site (ex. Site Assets, Style Library, Form Templates). Elles peuvent consommer beaucoup d'espace et sont souvent oubliées dans les audits de routine.
+ Bibliothèque de conservation
+ Bibliothèque SharePoint cachée qui stocke les versions de documents modifiés ou supprimés pendant qu'une politique de rétention Microsoft Purview / Microsoft 365 Compliance est active. Elle peut croître considérablement sans être visible pour les utilisateurs du site.
diff --git a/SharepointToolbox/Localization/Strings.resx b/SharepointToolbox/Localization/Strings.resx
index 74bde2a..403a60e 100644
--- a/SharepointToolbox/Localization/Strings.resx
+++ b/SharepointToolbox/Localization/Strings.resx
@@ -583,6 +583,8 @@ This cannot be undone.
Export OptionsMerge duplicate permissionsHide raw system group names (SharingLinks, Limited Access)
+ Exclude sharing links
+ Exclude system groups (Limited Access)Register AppRemove App
@@ -687,6 +689,7 @@ This cannot be undone.
GeneratedGenerated:members unavailable
+ Empty groupLink(no ext)(no extension)
@@ -784,4 +787,84 @@ This cannot be undone.
user(s)filessites
+ entries
+
+ Simplified Permissions Mode
+ Groups raw SharePoint permissions into readable labels (Owner, Editor, Contributor, Reader, View-Only) and color-codes rows by risk level. Useful for a quick security overview without permission-level jargon.
+ Merge Permissions
+ When enabled, multiple permission entries for the same user or group are consolidated into a single row in the export, reducing report size. Disable to see every individual permission assignment separately.
+ Hide System Groups
+ Removes automatically-created SharePoint system groups from results (e.g. "Excel Services Viewers", "SharingLinks.*" groups). These groups are managed internally by SharePoint and are typically not relevant for user access audits.
+ Exclude Sharing Links
+ Removes sharing link entries from results and exports (e.g. "Anyone with the link", organisation-wide links). Useful when you only care about direct user and group permissions.
+ Exclude System Groups (Limited Access)
+ Removes "Limited Access System Group For Web/List" entries from results and exports. SharePoint creates these automatically when a user has item-level access; they are rarely relevant for user access audits.
+ Include Inherited Permissions
+ By default only objects with unique (broken) permissions are reported. Enable this to also include objects that inherit permissions from a parent, giving a complete picture of who can access every item.
+ Export Split Mode
+ Single File: all results are saved in one CSV or HTML file.
+
+Split by Site: creates a separate file for each site collection. Useful when auditing large multi-site tenants to keep individual files manageable.
+ KQL File Search
+ Searches files across your SharePoint sites using KQL (Keyword Query Language). The keyword field is optional — leave it empty to return all files matching only the active filters. Combine date range, author, and library filters to narrow results.
+ Filename Regex Filter
+ Post-filters results client-side using a .NET regular expression matched against file names. Example: \.pdf$ matches only PDF files. Leave blank to skip this filter. The expression is case-insensitive.
+ Version Cleanup Policy
+ Permanently deletes old document versions from SharePoint libraries. Only the N most recent versions are kept — older ones are removed permanently and cannot be recovered. Run a preview scan first to see what will be deleted.
+ Keep First Version
+ Always preserves version 1.0 (the original) of each document, regardless of the "Keep Last N" setting. Useful to maintain an audit trail of a document's initial state.
+ Confirm Before Delete
+ When enabled, a confirmation dialog appears for each file before its versions are deleted. Uncheck for unattended batch processing.
+ Duplicate Matching Criteria
+ Two items are flagged as duplicates when their names match AND all checked additional criteria also match. More criteria checked = fewer groups, but more precise matches. Using name only finds files with the same filename anywhere in the site, regardless of content.
+ Include Source Folder
+ When enabled, the source folder itself is recreated at the destination (e.g. transferring "Reports" creates a "Reports/" folder at the target). When disabled, only the contents inside the folder are transferred — useful when merging into an existing destination folder.
+ Copy Folder Contents Only
+ When enabled, only the files and subfolders inside the selected folder are transferred — the selected folder itself is not recreated at the destination.
+ File Conflict Policy
+ Defines what happens when a file with the same name already exists at the destination:
+
+• Skip — leave the existing destination file unchanged.
+• Overwrite — replace the destination file with the source file.
+• Rename — keep both by appending a number suffix to the transferred file's name.
+ Bulk Add Members — CSV Format
+ The CSV file must contain these columns (headers required, order is flexible):
+• GroupName — the exact SharePoint group name
+• Email — the user's email address
+• Role — Member, Owner, or Visitor
+
+Click "Load Example" to open a pre-filled sample file.
+ Bulk Create Sites — CSV Format
+ The CSV file must contain these columns:
+• Name — the display name for the new site
+• Alias — URL alias (no spaces; becomes part of the site URL)
+• Type — TeamSite or CommunicationSite
+• Owners — comma-separated list of owner email addresses
+
+Click "Load Example" to open a pre-filled sample file.
+ Create Folder Structure — CSV Format
+ Creates a folder hierarchy inside a SharePoint library from a CSV file. Each row defines one folder path using up to 4 levels (Level1–Level4). Leave deeper level columns empty for shallower paths.
+
+Example row: Contracts | 2024 | Q1 | (empty)
+Creates: Library / Contracts / 2024 / Q1
+ Capture Site Template
+ Saves the currently selected site's structure (libraries, folder hierarchy, permissions, settings, and logo) as a reusable template stored locally on your machine. The source site is not modified in any way.
+
+Select which elements to include using the checkboxes above.
+ Apply Template to New Site
+ Creates a brand-new SharePoint site and reproduces the structure captured in the selected template — including libraries, folders, permissions, settings, and logo. The source template and original site are not affected.
+
+Provide a display name and URL alias for the new site before clicking Apply.
+ Search vs Browse Mode
+ Search Mode: type a name or email to find a specific user via Azure AD. Matching users appear in a list — click to select them for the audit.
+
+Browse Mode: loads all users in your tenant directory. Use the filter box to narrow the list, then double-click a row to add the user to the audit.
+ User Access Audit vs Permissions Audit
+ The Permissions tab scans objects (libraries, folders, items) and shows who has access to each one.
+
+This tab does the reverse: you select one or more users and it finds every object they can access — including access granted via SharePoint groups or Active Directory groups.
+ Hidden Libraries
+ Scans SharePoint libraries hidden from the site's normal navigation (e.g. Site Assets, Style Library, Form Templates). These can consume significant storage and are often overlooked in routine audits.
+ Preservation Hold Library
+ A hidden SharePoint library that stores versions of documents modified or deleted while a Microsoft Purview / Microsoft 365 Compliance retention policy is active. It can grow very large over time without being visible to normal site users.
diff --git a/SharepointToolbox/Services/BulkMemberService.cs b/SharepointToolbox/Services/BulkMemberService.cs
index c7aacfd..4efc0b0 100644
--- a/SharepointToolbox/Services/BulkMemberService.cs
+++ b/SharepointToolbox/Services/BulkMemberService.cs
@@ -64,6 +64,7 @@ public class BulkMemberService : IBulkMemberService
return;
}
}
+ catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
Log.Warning("Graph API failed for {GroupUrl}, falling back to CSOM: {Error}",
@@ -83,7 +84,7 @@ public class BulkMemberService : IBulkMemberService
{
// Resolve user by email
var user = await graphClient.Users[email].GetAsync(cancellationToken: ct);
- if (user == null)
+ if (user?.Id == null)
throw new InvalidOperationException($"User not found: {email}");
var userRef = $"https://graph.microsoft.com/v1.0/directoryObjects/{user.Id}";
@@ -138,13 +139,16 @@ public class BulkMemberService : IBulkMemberService
}
}
}
- catch { /* not a group-connected site */ }
+ catch (OperationCanceledException) { throw; }
+ catch (Exception ex) { Log.Debug("Group lookup not available for {SiteUrl}: {Error}", siteUrl, ex.Message); }
}
return null;
}
- catch
+ catch (OperationCanceledException) { throw; }
+ catch (Exception ex)
{
+ Log.Debug("Could not resolve M365 group ID for {SiteUrl}: {Error}", siteUrl, ex.Message);
return null;
}
}
diff --git a/SharepointToolbox/Services/BulkSiteService.cs b/SharepointToolbox/Services/BulkSiteService.cs
index 40a9936..077618b 100644
--- a/SharepointToolbox/Services/BulkSiteService.cs
+++ b/SharepointToolbox/Services/BulkSiteService.cs
@@ -54,6 +54,9 @@ public class BulkSiteService : IBulkSiteService
var owners = ParseEmails(row.Owners);
var members = ParseEmails(row.Members);
+ if (owners.Count == 0)
+ throw new InvalidOperationException($"Team site '{row.Name}' requires at least one owner.");
+
var creationInfo = new TeamSiteCollectionCreationInformation
{
DisplayName = row.Name,
@@ -88,6 +91,7 @@ public class BulkSiteService : IBulkSiteService
membersGroup.Users.AddUser(user);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct);
}
+ catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
Log.Warning("Failed to add member {Email} to {Site}: {Error}",
@@ -142,6 +146,7 @@ public class BulkSiteService : IBulkSiteService
ownersGroup.Users.AddUser(user);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct);
}
+ catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
Log.Warning("Failed to add owner {Email} to {Site}: {Error}",
@@ -162,6 +167,7 @@ public class BulkSiteService : IBulkSiteService
membersGroup.Users.AddUser(user);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct);
}
+ catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
Log.Warning("Failed to add member {Email} to {Site}: {Error}",
diff --git a/SharepointToolbox/Services/Export/HtmlExportService.cs b/SharepointToolbox/Services/Export/HtmlExportService.cs
index 4aeb3ca..9fb19c9 100644
--- a/SharepointToolbox/Services/Export/HtmlExportService.cs
+++ b/SharepointToolbox/Services/Export/HtmlExportService.cs
@@ -122,36 +122,46 @@ public class HtmlExportService
AppendFilterInput(sb);
AppendTableOpen(sb);
sb.AppendLine("
");
- sb.AppendLine($"
{T["report.col.object"]}
{T["report.col.title"]}
{T["report.col.url"]}
{T["report.badge.unique"]}
{T["report.col.users_groups"]}
{T["report.col.permission_level"]}
{T["report.col.simplified"]}
{T["report.col.risk"]}
{T["report.col.granted_through"]}
");
+ sb.AppendLine($"
{T["report.col.users_groups"]}
{T["report.col.permission_level"]}
{T["report.col.simplified"]}
{T["report.col.risk"]}
{T["report.col.granted_through"]}
");
sb.AppendLine("
");
sb.AppendLine("
");
int grpMemIdx = 0;
- foreach (var entry in entries)
+ int sectionIdx = 0;
+ var groups = entries.GroupBy(e => (e.ObjectType, e.Title, e.Url)).ToList();
+ foreach (var group in groups)
{
- var typeCss = ObjectTypeCss(entry.ObjectType);
- var uniqueCss = entry.HasUniquePermissions ? "badge unique" : "badge inherited";
- var uniqueLbl = entry.HasUniquePermissions ? T["report.badge.unique"] : T["report.badge.inherited"];
- var (riskBg, riskText, riskBorder) = RiskLevelColors(entry.RiskLevel);
+ var sectionId = $"sec{sectionIdx++}";
+ var first = group.First();
+ var typeCss = ObjectTypeCss(group.Key.ObjectType);
+ var uniqueCss = first.HasUniquePermissions ? "badge unique" : "badge inherited";
+ var uniqueLbl = first.HasUniquePermissions ? T["report.badge.unique"] : T["report.badge.inherited"];
+ var count = group.Count();
- var (pills, subRows) = BuildUserPillsCell(
- entry.UserLogins, entry.Inner.Users, entry.Inner.PrincipalType, groupMembers,
- colSpan: 9, grpMemIdx: ref grpMemIdx,
- targetLabel: entry.TargetLabel, sharingLinkType: entry.SharingLinkType,
- hideSystemGroupRaw: hideSystemGroupRaw);
-
- sb.AppendLine("