Back to Checks
vercel

Vercel team has no stale pending invitations older than 30 days

team_no_stale_invitations
Service: teamSeverity: low
OfficialBuilt-inVerified
Categories:trust-boundariesvercel-hobby-plan
Resource Type:NotDefined

Vercel team is assessed for stale invitations by checking whether pending invitations have been outstanding for more than 30 days. Stale invitations may indicate abandoned onboarding processes or forgotten invitation links that could be exploited.

Risk

Stale pending invitations represent unresolved access grants. If invitation links are intercepted or forwarded to unintended recipients, they could be used to gain unauthorized access to the team. Old invitations also indicate poor access lifecycle management.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler vercel --checks team_no_stale_invitations

Recommendation

Regularly review and revoke stale team invitations. Establish a process to follow up on pending invitations within a reasonable timeframe and revoke those that are no longer needed to reduce the risk of unauthorized access.

Remediation

Other

1Navigate to your Vercel team settings
2Go to the Members section
3Review all pending invitations
4Revoke any invitations that have been pending for more than 30 days
5Re-invite members if still needed

Check Code

from datetime import datetime, timezone
from typing import List

from prowler.lib.check.models import Check, CheckReportVercel
from prowler.providers.vercel.services.team.team_client import team_client


class team_no_stale_invitations(Check):
    """Check if the Vercel team has stale pending invitations.

    This class verifies that no team invitations have been pending for
    more than 30 days, which may indicate abandoned or forgotten invitations
    that should be revoked.
    """

    def execute(self) -> List[CheckReportVercel]:
        """Execute the Vercel Team No Stale Invitations check.

        Iterates over all teams and checks for pending invitations older
        than 30 days.

        Returns:
            List[CheckReportVercel]: A list of reports for each team.
        """
        findings = []
        for team in team_client.teams.values():
            report = CheckReportVercel(
                metadata=self.metadata(),
                resource=team,
                resource_name=team.name,
                resource_id=team.id,
            )

            now = datetime.now(timezone.utc)
            stale_threshold_days = team_client.audit_config.get(
                "stale_invitation_threshold_days", 30
            )
            stale_invitations = []

            for member in team.members:
                if member.status != "invited":
                    continue
                if member.created_at is None:
                    continue

                # Ensure created_at is timezone-aware for comparison
                created_at = member.created_at
                if created_at.tzinfo is None:
                    created_at = created_at.replace(tzinfo=timezone.utc)

                age_days = (now - created_at).days
                if age_days > stale_threshold_days:
                    stale_invitations.append(member)

            if not stale_invitations:
                report.status = "PASS"
                report.status_extended = (
                    f"Team {team.name} has no stale pending invitations "
                    f"older than {stale_threshold_days} days."
                )
            else:
                report.status = "FAIL"
                report.status_extended = (
                    f"Team {team.name} has {len(stale_invitations)} stale "
                    f"pending invitation(s) older than {stale_threshold_days} days."
                )

            findings.append(report)

        return findings