Skip to content

Audit of Logins

Recently I had to review all logins on a server and get telemetry data related to them. The below is what I settled on. In the where clause at the end, you can choose what kind of accounts you wish to look at. I knew I wanted to omit the system accounts beginning with ## and at the time I only looked for SQL Auth accounts where are type S. I included U and G for Windows User and Windows Groups. You could also include C or K for certificates and keys, or you could include E and X for AzureAD groups and users. I ran this against a Registered Server Group to tackle all the servers at once and give the client a nice output.

-- Create temp table for database permissions
CREATE TABLE #DatabasePermissions (
    LoginName NVARCHAR(128) COLLATE SQL_Latin1_General_CP1_CI_AS,
    DatabaseName NVARCHAR(128) COLLATE SQL_Latin1_General_CP1_CI_AS,
    DatabaseRoles NVARCHAR(MAX) COLLATE SQL_Latin1_General_CP1_CI_AS,
    ExplicitPermissions NVARCHAR(MAX) COLLATE SQL_Latin1_General_CP1_CI_AS
);

-- Build and execute dynamic SQL to populate database permissions
DECLARE @sql NVARCHAR(MAX) = '';

SELECT @sql = @sql + 
    'INSERT INTO #DatabasePermissions (LoginName, DatabaseName, DatabaseRoles, ExplicitPermissions)
     SELECT dp.name COLLATE SQL_Latin1_General_CP1_CI_AS as LoginName,
            ''' + name + ''' as DatabaseName,
            STUFF((
                SELECT '', '' + r.name COLLATE SQL_Latin1_General_CP1_CI_AS
                FROM [' + name + '].sys.database_role_members rm
                INNER JOIN [' + name + '].sys.database_principals r ON rm.role_principal_id = r.principal_id
                WHERE rm.member_principal_id = dp.principal_id
                FOR XML PATH(''''), TYPE
            ).value(''.'', ''NVARCHAR(MAX)''), 1, 2, '''') AS DatabaseRoles,
            STUFF((
                SELECT '', '' + p.permission_name COLLATE SQL_Latin1_General_CP1_CI_AS + '' ('' + p.state_desc COLLATE SQL_Latin1_General_CP1_CI_AS + '')''
                FROM [' + name + '].sys.database_permissions p
                WHERE p.grantee_principal_id = dp.principal_id
                  AND p.major_id = 0
                FOR XML PATH(''''), TYPE
            ).value(''.'', ''NVARCHAR(MAX)''), 1, 2, '''') AS ExplicitPermissions
     FROM [' + name + '].sys.database_principals dp
     WHERE dp.type = ''S'' 
       AND dp.name COLLATE SQL_Latin1_General_CP1_CI_AS IN (
           SELECT name COLLATE SQL_Latin1_General_CP1_CI_AS 
           FROM sys.server_principals 
           WHERE type = ''S'' 
           AND name NOT LIKE ''##%''
       );
     '
FROM sys.databases d
WHERE d.state = 0  -- Only online databases
  AND d.name NOT IN ('tempdb')
  -- Only include databases that are primary on this node
  AND (
      -- For Always On AG databases: only if primary replica
      NOT EXISTS (
          SELECT 1 FROM sys.availability_databases_cluster adc 
          JOIN sys.dm_hadr_availability_replica_states rs ON adc.group_id = rs.group_id
          WHERE adc.database_name = d.name 
          AND rs.is_local = 1 
          AND rs.role_desc != 'PRIMARY'
      )
      -- For mirrored databases: only if principal (mirroring_role = 1) or not mirrored
      AND NOT EXISTS (
          SELECT 1 FROM sys.database_mirroring dm 
          WHERE dm.database_id = d.database_id 
          AND dm.mirroring_role = 2  -- 2 = Mirror (secondary)
      )
      -- For log shipping: exclude if it's a secondary (read-only with restricted_user access)
      AND NOT (d.is_read_only = 1 AND d.user_access_desc = 'RESTRICTED_USER')
  );

-- Execute the dynamic SQL to populate temp table
EXEC sp_executesql @sql;

-- Main query with database permissions joined
SELECT 
    -- Login Information
    sp.name COLLATE SQL_Latin1_General_CP1_CI_AS AS LoginName,
    sp.type_desc AS LoginType,
    sp.is_disabled AS IsDisabled,
    sp.create_date AS CreateDate,
    sp.modify_date AS ModifyDate,
    sp.default_database_name AS DefaultDatabase,
    sp.default_language_name AS DefaultLanguage,
    
    -- SQL Login specific information (from sys.sql_logins)
    sl.is_policy_checked AS PasswordPolicyEnabled,
    sl.is_expiration_checked AS PasswordExpirationEnabled,
    CASE 
        WHEN LOGINPROPERTY(sp.name, 'IsLocked') IS NULL THEN 'Not Applicable'
        WHEN LOGINPROPERTY(sp.name, 'IsLocked') = 1 THEN 'Locked'
        ELSE 'Unlocked'
    END AS LockStatus,
    
    -- Password Information (for SQL logins)
    CASE 
        WHEN sl.password_hash IS NULL THEN 'N/A'
        WHEN PWDCOMPARE('', sl.password_hash) = 1 THEN 'Empty Password'
        WHEN PWDCOMPARE(sp.name COLLATE SQL_Latin1_General_CP1_CI_AS, sl.password_hash) = 1 THEN 'Password Same as Login'
        ELSE 'Password Set'
    END AS PasswordStatus,
    
    -- Server Role Memberships (compatible with SQL Server 2012+)
    STUFF((
        SELECT ', ' + sr2.name COLLATE SQL_Latin1_General_CP1_CI_AS
        FROM sys.server_role_members srm2
        INNER JOIN sys.server_principals sr2 ON srm2.role_principal_id = sr2.principal_id
        WHERE srm2.member_principal_id = sp.principal_id
        FOR XML PATH(''), TYPE
    ).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS ServerRoles,
    
    -- Database Level Permissions (formatted as: DB1: role1, role2; DB2: role3)
    STUFF((
        SELECT '; ' + 
               CASE 
                   WHEN dp.DatabaseRoles IS NOT NULL AND dp.ExplicitPermissions IS NOT NULL 
                   THEN dp.DatabaseName + ': ' + dp.DatabaseRoles + ' | ' + dp.ExplicitPermissions
                   WHEN dp.DatabaseRoles IS NOT NULL 
                   THEN dp.DatabaseName + ': ' + dp.DatabaseRoles
                   WHEN dp.ExplicitPermissions IS NOT NULL 
                   THEN dp.DatabaseName + ': ' + dp.ExplicitPermissions
                   ELSE dp.DatabaseName + ': (access only)'
               END
        FROM #DatabasePermissions dp
        WHERE dp.LoginName COLLATE SQL_Latin1_General_CP1_CI_AS = sp.name COLLATE SQL_Latin1_General_CP1_CI_AS
        FOR XML PATH(''), TYPE
    ).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS DatabasePermissions

FROM sys.server_principals sp
    
    -- Join with sys.sql_logins for SQL authentication specific columns
    LEFT JOIN sys.sql_logins sl ON sp.principal_id = sl.principal_id

WHERE 
    -- Filter for SQL Server Authentication logins only
    sp.type IN ('S','U','G')
    AND sp.name NOT LIKE '##%'  -- Exclude system logins
    
ORDER BY sp.name COLLATE SQL_Latin1_General_CP1_CI_AS;

-- Clean up temp table
DROP TABLE #DatabasePermissions;