@@ -849,3 +849,377 @@ TEST_F(EnsureFilePermissionsTest, AuditCollectionRecurseTrueNoneExists)
849849
850850 ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" , behavior none_exist" ) != std::string::npos);
851851}
852+
853+ TEST_F (EnsureFilePermissionsTest, AuditCollectionRecurseTrueOnlyOneExists)
854+ {
855+ // Create top-level compliant file
856+ CreateFileInDir (" top.txt" , 0 , 0 , 0644 );
857+ // Create nested directory and a non-compliant file inside it (wrong owner)
858+ std::string nestedDir = testDir + " /nested2" ;
859+ ASSERT_EQ (mkdir (nestedDir.c_str (), 0755 ), 0 );
860+
861+ {
862+ std::string nestedFile = nestedDir + " /good1.txt" ;
863+ std::ofstream nf (nestedFile);
864+ nf << " content" ;
865+ nf.close ();
866+ ASSERT_EQ (chmod (nestedFile.c_str (), 0644 ), 0 );
867+ ASSERT_EQ (chown (nestedFile.c_str (), 0 , 0 ), 0 );
868+ files.push_back (nestedFile);
869+ }
870+
871+ {
872+ std::string nestedFile = nestedDir + " /good2.txt" ;
873+ std::ofstream nf (nestedFile);
874+ nf << " content" ;
875+ nf.close ();
876+ ASSERT_EQ (chmod (nestedFile.c_str (), 0644 ), 0 );
877+ ASSERT_EQ (chown (nestedFile.c_str (), 0 , 0 ), 0 );
878+ files.push_back (nestedFile);
879+ }
880+ EnsureFilePermissionsCollectionParams params;
881+ params.directory = testDir;
882+ params.ext = " *.txt" ; // Matches both files but recurse true and Behavior OnlyOneExists
883+ auto owner = Pattern::Make (" root" );
884+ ASSERT_TRUE (owner.HasValue ());
885+ params.owner = {{std::move (owner).Value ()}};
886+ params.permissions = 0644 ;
887+ params.recurse = true ;
888+ params.behavior = Behavior::OnlyOneExists;
889+
890+ auto result = AuditEnsureFilePermissionsCollection (params, indicators, mContext );
891+ ASSERT_TRUE (result.HasValue ());
892+ ASSERT_EQ (result.Value (), Status::Compliant);
893+
894+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" , behavior only_one_exists" ) != std::string::npos);
895+ }
896+
897+ TEST_F (EnsureFilePermissionsTest, AuditCollectionRecurseTrueOnlyOneExistsOneBad)
898+ {
899+ // Create top-level compliant file
900+ CreateFileInDir (" top.txt" , 0 , 0 , 0644 );
901+ // Create nested directory and a non-compliant file inside it (wrong owner)
902+ std::string nestedDir = testDir + " /nested2" ;
903+ ASSERT_EQ (mkdir (nestedDir.c_str (), 0755 ), 0 );
904+
905+ {
906+ std::string nestedFile = nestedDir + " /good1.txt" ;
907+ std::ofstream nf (nestedFile);
908+ nf << " content" ;
909+ nf.close ();
910+ ASSERT_EQ (chmod (nestedFile.c_str (), 0644 ), 0 );
911+ ASSERT_EQ (chown (nestedFile.c_str (), 0 , 0 ), 0 );
912+ files.push_back (nestedFile);
913+ }
914+
915+ {
916+ std::string nestedFile = nestedDir + " /bad.txt" ;
917+ std::ofstream nf (nestedFile);
918+ nf << " content" ;
919+ nf.close ();
920+ ASSERT_EQ (chmod (nestedFile.c_str (), 0644 ), 0 );
921+ ASSERT_EQ (chown (nestedFile.c_str (), 1 , 0 ), 0 );
922+ files.push_back (nestedFile);
923+ }
924+ EnsureFilePermissionsCollectionParams params;
925+ params.directory = testDir;
926+ params.ext = " *.txt" ; // Matches both files but recurse true and Behavior OnlyOneExists
927+ auto owner = Pattern::Make (" root" );
928+ ASSERT_TRUE (owner.HasValue ());
929+ params.owner = {{std::move (owner).Value ()}};
930+ params.permissions = 0644 ;
931+ params.recurse = true ;
932+ params.behavior = Behavior::OnlyOneExists;
933+
934+ auto result = AuditEnsureFilePermissionsCollection (params, indicators, mContext );
935+ ASSERT_TRUE (result.HasValue ());
936+ ASSERT_EQ (result.Value (), Status::NonCompliant);
937+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" , behavior only_one_exists" ) != std::string::npos);
938+ }
939+
940+ TEST_F (EnsureFilePermissionsTest, AuditCollectionRecurseTrueAtLeastOneExists)
941+ {
942+ // Create top-level compliant file
943+ CreateFileInDir (" top.txt" , 0 , 0 , 0644 );
944+ // Create nested directory and a non-compliant file inside it (wrong owner)
945+ std::string nestedDir = testDir + " /nested2" ;
946+ ASSERT_EQ (mkdir (nestedDir.c_str (), 0755 ), 0 );
947+
948+ {
949+ std::string nestedFile = nestedDir + " /good1.txt" ;
950+ std::ofstream nf (nestedFile);
951+ nf << " content" ;
952+ nf.close ();
953+ ASSERT_EQ (chmod (nestedFile.c_str (), 0644 ), 0 );
954+ ASSERT_EQ (chown (nestedFile.c_str (), 0 , 0 ), 0 );
955+ files.push_back (nestedFile);
956+ }
957+
958+ {
959+ std::string nestedFile = nestedDir + " /good2.txt" ;
960+ std::ofstream nf (nestedFile);
961+ nf << " content" ;
962+ nf.close ();
963+ ASSERT_EQ (chmod (nestedFile.c_str (), 0644 ), 0 );
964+ ASSERT_EQ (chown (nestedFile.c_str (), 0 , 0 ), 0 );
965+ files.push_back (nestedFile);
966+ }
967+ EnsureFilePermissionsCollectionParams params;
968+ params.directory = testDir;
969+ params.ext = " *.txt" ; // Matches both files but recurse true and Behavior OnlyOneExists
970+ auto owner = Pattern::Make (" root" );
971+ ASSERT_TRUE (owner.HasValue ());
972+ params.owner = {{std::move (owner).Value ()}};
973+ params.permissions = 0644 ;
974+ params.recurse = true ;
975+ params.behavior = Behavior::AtLeastOneExists;
976+
977+ auto result = AuditEnsureFilePermissionsCollection (params, indicators, mContext );
978+ ASSERT_TRUE (result.HasValue ());
979+ ASSERT_EQ (result.Value (), Status::Compliant);
980+
981+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" , behavior at_least_one_exists" ) != std::string::npos);
982+ }
983+ TEST_F (EnsureFilePermissionsTest, AuditCollectionRecurseTrueAtLeastOneExistsOneBad)
984+ {
985+ // Create top-level compliant file
986+ CreateFileInDir (" top.txt" , 0 , 0 , 0644 );
987+ // Create nested directory and a non-compliant file inside it (wrong owner)
988+ std::string nestedDir = testDir + " /nested2" ;
989+ ASSERT_EQ (mkdir (nestedDir.c_str (), 0755 ), 0 );
990+
991+ {
992+ std::string nestedFile = nestedDir + " /good1.txt" ;
993+ std::ofstream nf (nestedFile);
994+ nf << " content" ;
995+ nf.close ();
996+ ASSERT_EQ (chmod (nestedFile.c_str (), 0644 ), 0 );
997+ ASSERT_EQ (chown (nestedFile.c_str (), 0 , 0 ), 0 );
998+ files.push_back (nestedFile);
999+ }
1000+
1001+ {
1002+ std::string nestedFile = nestedDir + " /bad.txt" ;
1003+ std::ofstream nf (nestedFile);
1004+ nf << " content" ;
1005+ nf.close ();
1006+ ASSERT_EQ (chmod (nestedFile.c_str (), 0644 ), 0 );
1007+ ASSERT_EQ (chown (nestedFile.c_str (), 1 , 0 ), 0 );
1008+ files.push_back (nestedFile);
1009+ }
1010+ EnsureFilePermissionsCollectionParams params;
1011+ params.directory = testDir;
1012+ params.ext = " *.txt" ; // Matches both files but recurse true and Behavior OnlyOneExists
1013+ auto owner = Pattern::Make (" root" );
1014+ ASSERT_TRUE (owner.HasValue ());
1015+ params.owner = {{std::move (owner).Value ()}};
1016+ params.permissions = 0644 ;
1017+ params.recurse = true ;
1018+ params.behavior = Behavior::AtLeastOneExists;
1019+
1020+ auto result = AuditEnsureFilePermissionsCollection (params, indicators, mContext );
1021+ ASSERT_TRUE (result.HasValue ());
1022+ ASSERT_EQ (result.Value (), Status::NonCompliant);
1023+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" , behavior at_least_one_exists" ) != std::string::npos);
1024+ }
1025+
1026+ // ── Single-file: file exists, all Behavior values that are not NoneExist ──────
1027+
1028+ TEST_F (EnsureFilePermissionsTest, AuditFileExistsAnyExist)
1029+ {
1030+ EnsureFilePermissionsParams params;
1031+ CreateFile (params.filename , 0 , 0 , 0600 );
1032+ params.behavior = Behavior::AnyExist;
1033+
1034+ auto result = AuditEnsureFilePermissions (params, indicators, mContext );
1035+ ASSERT_TRUE (result.HasValue ());
1036+ ASSERT_EQ (result.Value (), Status::Compliant);
1037+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" correct permissions and ownership, behavior any_exist" ) != std::string::npos);
1038+ }
1039+
1040+ TEST_F (EnsureFilePermissionsTest, AuditFileExistsOnlyOneExists)
1041+ {
1042+ EnsureFilePermissionsParams params;
1043+ CreateFile (params.filename , 0 , 0 , 0600 );
1044+ params.behavior = Behavior::OnlyOneExists;
1045+
1046+ auto result = AuditEnsureFilePermissions (params, indicators, mContext );
1047+ ASSERT_TRUE (result.HasValue ());
1048+ ASSERT_EQ (result.Value (), Status::Compliant);
1049+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" correct permissions and ownership, behavior only_one_exists" ) != std::string::npos);
1050+ }
1051+
1052+ TEST_F (EnsureFilePermissionsTest, AuditFileExistsAtLeastOneExistsExplicit)
1053+ {
1054+ EnsureFilePermissionsParams params;
1055+ CreateFile (params.filename , 0 , 0 , 0600 );
1056+ params.behavior = Behavior::AtLeastOneExists;
1057+
1058+ auto result = AuditEnsureFilePermissions (params, indicators, mContext );
1059+ ASSERT_TRUE (result.HasValue ());
1060+ ASSERT_EQ (result.Value (), Status::Compliant);
1061+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" correct permissions and ownership, behavior at_least_one_exists" ) != std::string::npos);
1062+ }
1063+
1064+ // ── Single-file: file exists with wrong owner — behavior must not skip perms ──
1065+
1066+ TEST_F (EnsureFilePermissionsTest, AuditFileExistsBadPermsAnyExist)
1067+ {
1068+ EnsureFilePermissionsParams params;
1069+ CreateFile (params.filename , 1 , 0 , 0600 ); // owner=bin, not root
1070+ auto owner = Pattern::Make (" root" );
1071+ ASSERT_TRUE (owner.HasValue ());
1072+ params.owner = {{std::move (owner).Value ()}};
1073+ params.behavior = Behavior::AnyExist;
1074+
1075+ auto result = AuditEnsureFilePermissions (params, indicators, mContext );
1076+ ASSERT_TRUE (result.HasValue ());
1077+ ASSERT_EQ (result.Value (), Status::NonCompliant);
1078+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" owner" ) != std::string::npos);
1079+ }
1080+
1081+ TEST_F (EnsureFilePermissionsTest, AuditFileExistsBadPermsOnlyOneExists)
1082+ {
1083+ EnsureFilePermissionsParams params;
1084+ CreateFile (params.filename , 1 , 0 , 0600 ); // owner=bin, not root
1085+ auto owner = Pattern::Make (" root" );
1086+ ASSERT_TRUE (owner.HasValue ());
1087+ params.owner = {{std::move (owner).Value ()}};
1088+ params.behavior = Behavior::OnlyOneExists;
1089+
1090+ auto result = AuditEnsureFilePermissions (params, indicators, mContext );
1091+ ASSERT_TRUE (result.HasValue ());
1092+ ASSERT_EQ (result.Value (), Status::NonCompliant);
1093+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" owner" ) != std::string::npos);
1094+ }
1095+
1096+ // ── Collection: no matching files, all Behavior values ────────────────────────
1097+
1098+ TEST_F (EnsureFilePermissionsTest, AuditCollectionNoMatchingFilesNoneExist)
1099+ {
1100+ CreateFileInDir (" file1.log" , 0 , 0 , 0644 );
1101+
1102+ EnsureFilePermissionsCollectionParams params;
1103+ params.directory = testDir;
1104+ params.ext = " *.txt" ; // no txt files exist
1105+ params.behavior = Behavior::NoneExist;
1106+
1107+ auto result = AuditEnsureFilePermissionsCollection (params, indicators, mContext );
1108+ ASSERT_TRUE (result.HasValue ());
1109+ ASSERT_EQ (result.Value (), Status::Compliant);
1110+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" behavior none_exist" ) != std::string::npos);
1111+ }
1112+
1113+ TEST_F (EnsureFilePermissionsTest, AuditCollectionNoMatchingFilesAllExist)
1114+ {
1115+ CreateFileInDir (" file1.log" , 0 , 0 , 0644 );
1116+
1117+ EnsureFilePermissionsCollectionParams params;
1118+ params.directory = testDir;
1119+ params.ext = " *.txt" ;
1120+ params.behavior = Behavior::AllExist;
1121+
1122+ auto result = AuditEnsureFilePermissionsCollection (params, indicators, mContext );
1123+ ASSERT_TRUE (result.HasValue ());
1124+ ASSERT_EQ (result.Value (), Status::NonCompliant);
1125+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" behavior all_exist" ) != std::string::npos);
1126+ }
1127+
1128+ TEST_F (EnsureFilePermissionsTest, AuditCollectionNoMatchingFilesOnlyOneExists)
1129+ {
1130+ CreateFileInDir (" file1.log" , 0 , 0 , 0644 );
1131+
1132+ EnsureFilePermissionsCollectionParams params;
1133+ params.directory = testDir;
1134+ params.ext = " *.txt" ;
1135+ params.behavior = Behavior::OnlyOneExists;
1136+
1137+ auto result = AuditEnsureFilePermissionsCollection (params, indicators, mContext );
1138+ ASSERT_TRUE (result.HasValue ());
1139+ ASSERT_EQ (result.Value (), Status::NonCompliant);
1140+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" behavior only_one_exists" ) != std::string::npos);
1141+ }
1142+
1143+ TEST_F (EnsureFilePermissionsTest, AuditCollectionNoMatchingFilesAnyExist)
1144+ {
1145+ CreateFileInDir (" file1.log" , 0 , 0 , 0644 );
1146+
1147+ EnsureFilePermissionsCollectionParams params;
1148+ params.directory = testDir;
1149+ params.ext = " *.txt" ;
1150+ params.behavior = Behavior::AnyExist;
1151+
1152+ auto result = AuditEnsureFilePermissionsCollection (params, indicators, mContext );
1153+ ASSERT_TRUE (result.HasValue ());
1154+ ASSERT_EQ (result.Value (), Status::NonCompliant);
1155+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" behavior any_exist" ) != std::string::npos);
1156+ }
1157+
1158+ // ── Collection: AnyExist with matching files ──────────────────────────────────
1159+
1160+ TEST_F (EnsureFilePermissionsTest, AuditCollectionAnyExistAllGood)
1161+ {
1162+ CreateFileInDir (" file1.txt" , 0 , 0 , 0644 );
1163+ CreateFileInDir (" file2.txt" , 0 , 0 , 0644 );
1164+
1165+ EnsureFilePermissionsCollectionParams params;
1166+ params.directory = testDir;
1167+ params.ext = " *.txt" ;
1168+ auto owner = Pattern::Make (" root" );
1169+ ASSERT_TRUE (owner.HasValue ());
1170+ params.owner = {{std::move (owner).Value ()}};
1171+ params.permissions = 0644 ;
1172+ params.behavior = Behavior::AnyExist;
1173+
1174+ auto result = AuditEnsureFilePermissionsCollection (params, indicators, mContext );
1175+ ASSERT_TRUE (result.HasValue ());
1176+ ASSERT_EQ (result.Value (), Status::Compliant);
1177+ }
1178+
1179+ TEST_F (EnsureFilePermissionsTest, AuditCollectionAnyExistOneBad)
1180+ {
1181+ CreateFileInDir (" file1.txt" , 0 , 0 , 0644 ); // compliant
1182+ CreateFileInDir (" file2.txt" , 1 , 0 , 0644 ); // wrong owner
1183+
1184+ EnsureFilePermissionsCollectionParams params;
1185+ params.directory = testDir;
1186+ params.ext = " *.txt" ;
1187+ auto owner = Pattern::Make (" root" );
1188+ ASSERT_TRUE (owner.HasValue ());
1189+ params.owner = {{std::move (owner).Value ()}};
1190+ params.permissions = 0644 ;
1191+ params.behavior = Behavior::AnyExist;
1192+
1193+ auto result = AuditEnsureFilePermissionsCollection (params, indicators, mContext );
1194+ ASSERT_TRUE (result.HasValue ());
1195+ ASSERT_EQ (result.Value (), Status::NonCompliant);
1196+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" owner" ) != std::string::npos);
1197+ }
1198+
1199+ // ── Collection: missing directory, NoneExist vs AtLeastOneExists ──────────────
1200+
1201+ TEST_F (EnsureFilePermissionsTest, AuditCollectionMissingDirectoryNoneExist)
1202+ {
1203+ EnsureFilePermissionsCollectionParams params;
1204+ params.directory = " /tmp/this_dir_does_not_exist_efp_test_noneexist" ;
1205+ params.ext = " *.conf" ;
1206+ params.behavior = Behavior::NoneExist;
1207+
1208+ auto result = AuditEnsureFilePermissionsCollection (params, indicators, mContext );
1209+ ASSERT_TRUE (result.HasValue ());
1210+ ASSERT_EQ (result.Value (), Status::Compliant);
1211+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" behavior none_exist" ) != std::string::npos);
1212+ }
1213+
1214+ TEST_F (EnsureFilePermissionsTest, AuditCollectionMissingDirectoryAtLeastOneExists)
1215+ {
1216+ EnsureFilePermissionsCollectionParams params;
1217+ params.directory = " /tmp/this_dir_does_not_exist_efp_test_atleastone" ;
1218+ params.ext = " *.conf" ;
1219+ params.behavior = Behavior::AtLeastOneExists;
1220+
1221+ auto result = AuditEnsureFilePermissionsCollection (params, indicators, mContext );
1222+ ASSERT_TRUE (result.HasValue ());
1223+ ASSERT_EQ (result.Value (), Status::NonCompliant);
1224+ ASSERT_TRUE (mFormatter .Format (indicators).Value ().find (" behavior at_least_one_exists" ) != std::string::npos);
1225+ }
0 commit comments