Steps to Reproduce
In an M365 tenant with more than 100 total groups, run any check that iterates admincenter_client.groups, e.g.:
prowler m365 --check admincenter_groups_not_public_visibility
Expected behavior
A finding (PASS or FAIL) for every Unified group in the tenant.
Actual Result with Screenshots or Logs
Only Unified groups that land on Graph's first /groups page (default 100 items, unfiltered) are reported. Anything past page 1 is silently dropped — neither PASS nor FAIL.
Repro tenant: 164 total groups, 31 Unified. admincenter_groups_not_public_visibility produced 18 findings (16 Unified groups found on page 1, plus duplicates from a re-run); 13 Unified groups, including visibility=Public ones that should have been flagged FAIL, are missing entirely.
Graph confirms @odata.nextLink is set on the first response:
$ curl -sS -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/groups?\$top=100" | jq '"@odata.nextLink" | length'
1 # next page exists, Prowler does not follow it
Root cause
prowler/providers/m365/services/admincenter/admincenter_service.py, _get_groups (lines 172–194 on main at bcd282d3d):
async def _get_groups(self):
...
groups_list = await self.client.groups.get()
for group in groups_list.value: # only first page
groups.update({...})
# no while-loop, no odata_next_link follow-up
_get_users in the same file does paginate (added by PR #8858). The same pattern needs to be applied to _get_groups. _get_directory_roles and the /domains iteration in _get_password_policy have the same single-page pattern and should be paginated for consistency, though the practical impact there is small.
Suggested fix
Mirror _get_users:
async def _get_groups(self):
logger.info("M365 - Getting groups...")
groups = {}
try:
groups_response = await self.client.groups.get()
while groups_response:
for group in getattr(groups_response, "value", []) or []:
groups.update({
group.id: Group(
id=group.id,
name=getattr(group, "display_name", ""),
visibility=getattr(group, "visibility", ""),
group_types=getattr(group, "group_types", []) or [],
)
})
next_link = getattr(groups_response, "odata_next_link", None)
if not next_link:
break
groups_response = await self.client.groups.with_url(next_link).get()
except Exception as error:
logger.error(f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}")
return groups
Verified on the repro tenant: admincenter_groups_not_public_visibility jumped from 18 to 30+ findings and the previously-missing Public groups now correctly appear as FAIL. Existing unit tests in tests/providers/m365/services/admincenter/ all pass against the patched module.
Happy to send a PR.
How did you install Prowler?
Docker (prowlercloud/prowler-api:5.27.0).
Prowler version
prowler 5.27.0 — also present on main at HEAD (bcd282d3d).
Steps to Reproduce
In an M365 tenant with more than 100 total groups, run any check that iterates
admincenter_client.groups, e.g.:Expected behavior
A finding (PASS or FAIL) for every Unified group in the tenant.
Actual Result with Screenshots or Logs
Only Unified groups that land on Graph's first
/groupspage (default 100 items, unfiltered) are reported. Anything past page 1 is silently dropped — neither PASS nor FAIL.Repro tenant: 164 total groups, 31 Unified.
admincenter_groups_not_public_visibilityproduced 18 findings (16 Unified groups found on page 1, plus duplicates from a re-run); 13 Unified groups, includingvisibility=Publicones that should have been flagged FAIL, are missing entirely.Graph confirms
@odata.nextLinkis set on the first response:Root cause
prowler/providers/m365/services/admincenter/admincenter_service.py,_get_groups(lines 172–194 onmainatbcd282d3d):_get_usersin the same file does paginate (added by PR #8858). The same pattern needs to be applied to_get_groups._get_directory_rolesand the/domainsiteration in_get_password_policyhave the same single-page pattern and should be paginated for consistency, though the practical impact there is small.Suggested fix
Mirror
_get_users:Verified on the repro tenant:
admincenter_groups_not_public_visibilityjumped from 18 to 30+ findings and the previously-missing Public groups now correctly appear as FAIL. Existing unit tests intests/providers/m365/services/admincenter/all pass against the patched module.Happy to send a PR.
How did you install Prowler?
Docker (
prowlercloud/prowler-api:5.27.0).Prowler version
prowler5.27.0 — also present onmainat HEAD (bcd282d3d).