Don't try to do this as one regex. A single expression is hard to prove correct or maintain over time, and when it doesn't match you don't know which part of the expression caused the failure. Instead, have one expression per rule. Put the expressions in a collection like a list, and then validate that every expression in the collection is okay.
This way each expression is easy to write and maintain, you can easily change which rules you need to use over time, and you can tell the user exactly what they did wrong.
When you get to this point, you may also find that regex isn't always the best way to write the rule, and instead you want a collection of string predicates (think lambda expressions that accept a string and return a bool).
So you might have code like this to create your rule set:
var passwordRules = new List<Predicate<string>> {
s => s.Length >= 8, //length at least 8
s => Regex.IsMatch(s, "[0-9]"), //digit
s => Regex.IsMatch(s, "[A-Z]") && Regex.IsMatch(s, "[a-z]"), //mix upercase/lowercase
s => Regex.IsMatch(s, "[^A-Za-z0-9]"), //special character
s => Char.IsLetter(s[0]) // first character cannot be numeric or special
};
and then validate the rules like this:
bool result = passwordRules.All(r => r(password));
Note that of the five rules I shared, three of them do use regular expressions, but two do not.
To find out which rules fail:
var failedRules = passwordRules.Where(r => !r(password));
Where no items in the result means it passes. You could combine this with a class or tuple to get a string message here to show to the user:
var passwordRules = new List<(string, Predicate<string>)> {
("Must have at least 8 characters", s => s.Length >= 8),
("Must include a digit", s => Regex.IsMatch(s, "[0-9]")),
("Must mix uppercase and lowercase characters", s => Regex.IsMatch(s, "[A-Z]") && Regex.IsMatch(s, "[a-z]")),
("Must use a special character", s => Regex.IsMatch(s, "[^A-Za-z0-9]")),
("First character cannot be numeric or special character", s => Char.IsLetter(s[0]))
};
var messages = passwordRules.
Where(r => !r.Item2(password)).
Select(r => r.Item1);
if (messages.Count() > 0)
{
Console.WriteLine(string.Join("
", messages));
}
else
{
//Success!
}
See it work here:
https://dotnetfiddle.net/c6Tqz7
Speaking of changing the rules, most of the specific rules in the question are now considered poor practice. We're no longer supposed to use complexity rules for password validation. No more UPPER-case, numeric, or special character requirements! Also, less frequent password change requirements.
Instead, we're supposed to do three new things.
First, we're supposed to ask users to make the passwords longer. Instead of an 8 character minimum, ask for a minimum of 10 or even 12 characters. Second, we're supposed to check the password against a list of commonly used or previously breached passwords and reject passwords in the list. Finally, we're supposed do these checks in real-time (per-character entry) and provide immediate feedback to the user on the quality of the entered password. An optional fourth test is checking the password again the name, username, and other known personal information; don't just let the user repeat their username, SSN, or birthdate (for example) as their password.
This newer scheme is backed up by strong research and promoted by, among others, the US Department of Defense. It's easier on users and harder (much harder) on attackers. Unfortunately, some industry groups haven't caught up with the new standards yet, and are still stuck in the old-and-busted complexity era (credit card companies and financial industry, I'm looking at you). But if this isn't your industry, you should move towards the newer password scheme.